ARTICLE DETAIL

资讯详情

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

JavaWeb超市管理系统实战:从环境搭建到库存扣减与答辩避坑

JavaWeb超市管理系统实战:从环境搭建到库存扣减与答辩避坑 简介这是一套面向计算机、通信、人工智能、自动化等相关专业学生与教师的JavaWeb超市管理系统毕业设计资源包含完整项目源码与数据库脚本适合作为毕业设计、期末课程设计或课程大作业的参考方案也便于基础较好的学习者在此基础上修改调整、扩展功能。资源包共64个文件以28个java源文件、21个jsp页面、10个xml配置、1个properties配置、1个sql数据库脚本及少量图片和说明文档为主涵盖后端业务逻辑、前端页面展示与数据库建表脚本压缩包整体约582KB结构紧凑、便于查阅。目前已有141人学习下载。项目为个人毕设作品答辩评审分达到98分代码经过调试测试可正常运行读者可据此快速理解JavaWeb项目的分层结构、页面交互与数据库设计思路并对照源码完成环境搭建与功能验证对入门学习与进阶提升均有较高借鉴价值。1. 超市管理系统从课程设计到能跑起来的 JavaWeb 项目差在哪很多同学做毕业设计选题定了“基于 JavaWeb 的超市管理系统”第一反应是去搜一套源码下载下来用 IDEA 打开配好 Tomcat跑起来看到登录页就觉得事情成了一半。但真正开始改代码、写论文、准备答辩的时候才发现问题一个接一个数据库脚本导入报错、JDBC 连接拿不到、JSP 页面中文乱码、分页查询逻辑看不懂、库存扣减对不上账。这套系统看起来简单实际上把 JavaWeb 的核心链路——Servlet 请求处理、JSP 视图渲染、JDBC 数据库操作、Session 会话管理——全串了一遍。它适合计算机相关专业的毕业生用来完成毕设也适合刚学完 JavaWeb 想找一个完整案例练手的人。关键在于你不能只满足于“跑起来”得知道每一层在干什么数据库脚本里每张表为什么这么设计改一个字段会牵动哪些页面。这篇笔记就按这个思路把一套典型的超市管理系统从环境搭建到核心业务实现再到常见翻车点完整拆一遍。2. 环境搭建与数据库脚本导入别在第一步就卡住2.1 开发环境选型与 IDEA 配置要点做 JavaWeb 项目环境版本不匹配是最大的玄学问题。我一般会锁定一套经过验证的组合JDK 8 或 JDK 11Tomcat 8.5 或 9.0MySQL 5.7 或 8.0IDEA 用 2021 之后的版本都行。为什么强调版本因为 Tomcat 10 把 javax.servlet 包名改成了 jakarta.servlet很多老项目的 Servlet 代码直接编译不过。如果你拿到的源码是几年前写的用 Tomcat 10 跑控制台会报 ClassNotFoundException: javax.servlet.http.HttpServlet这时候别怀疑代码先换 Tomcat 9。在 IDEA 里配置项目核心是三步Project Structure 里设好 SDK 和语言级别Facets 里确认 Web 模块的 web.xml 路径和 Web 资源目录Artifacts 里把编译输出和 lib 依赖打进 WAR 包。常见坑是 Artifacts 里只放了 classes 目录没把 MySQL 驱动 jar 包加进去结果 Tomcat 启动时报 No suitable driver found for jdbc:mysql。解决办法是在 Artifacts 的 Output Layout 里把 lib 目录下的 jar 全部添加到 WEB-INF/lib 下。数据库连接池一般用 Druid 或者 C3P0源码里如果用的是原生 JDBC每次请求都新建连接并发一上来就崩。我建议至少换成 Druid配置一个 druid.properties 放在 src 根目录内容如下driverClassNamecom.mysql.cj.jdbc.Driver urljdbc:mysql://localhost:3306/supermarket?useUnicodetruecharacterEncodingutf8serverTimezoneAsia/ShanghaiuseSSLfalse usernameroot password你的密码 initialSize5 maxActive20 maxWait3000这里几个参数值得说清楚useUnicode 和 characterEncoding 一起用才能保证中文不乱码serverTimezone 必须设否则 MySQL 8 会报时区错误useSSLfalse 在本地开发时关掉省去证书配置的麻烦。initialSize 是连接池启动时创建的连接数maxActive 是最大连接数maxWait 是获取连接的超时时间单位毫秒。这几个值根据你机器配置和预期并发调毕设演示场景 initialSize 设 5 足够。2.2 数据库脚本导入与表结构核对拿到数据库脚本别急着直接 source 执行。先打开看一眼确认几个东西字符集是不是 utf8mb4存储引擎是不是 InnoDB有没有外键约束。我见过不少脚本用 MyISAM不支持事务后面做库存扣减的时候数据对不上查半天查不出来。导入脚本的标准操作# 登录 MySQL mysql -u root -p # 创建数据库并指定字符集 CREATE DATABASE supermarket DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; # 切换数据库 USE supermarket; # 导入脚本 source /path/to/supermarket.sql;执行完之后用SHOW TABLES;看一下表是否齐全。一个典型的超市管理系统至少包含这几张核心表用户表user、商品表product、商品分类表category、供应商表supplier、库存表inventory、销售订单表sale_order、销售明细表sale_detail。每张表的字段设计直接决定了后面业务代码怎么写。以商品表为例常见字段包括product_id主键自增、product_name商品名称、category_id外键关联分类表、supplier_id外键关联供应商表、purchase_price进价、sale_price售价、stock库存数量、unit单位、create_time创建时间。这里有个设计细节库存数量到底放在商品表里还是单独建一张库存表两种做法各有道理。放商品表里查询方便但每次出入库都要更新商品表锁竞争高单独建库存表可以记录每次变动流水方便对账但查询时要多一次 join。毕设项目一般放商品表里就够了但如果你想让论文看起来更有深度可以加一张库存变动记录表记录每次入库、出库、退货的操作。导入完成后建议手动插入几条测试数据别用空表跑。空表跑分页查询看不出问题一页显示 0 条你以为是代码错了其实是没数据。INSERT INTO product (product_name, category_id, supplier_id, purchase_price, sale_price, stock, unit) VALUES (可口可乐 330ml, 1, 1, 2.50, 3.50, 100, 瓶), (康师傅红烧牛肉面, 2, 2, 3.00, 4.50, 200, 袋), (心相印抽纸, 3, 3, 5.00, 8.00, 50, 提);插入之后用 SELECT 查一下确认中文没变成问号。如果变成问号检查数据库、表、连接串三处的字符集是否统一为 utf8mb4。3. 核心业务链路从登录到下单代码怎么写才不翻车3.1 登录模块Session 管理与过滤器配置登录是系统的入口也是 Session 用得最多的地方。典型流程是用户提交用户名密码Servlet 调用 Service 层查询数据库比对成功后将用户信息存入 HttpSession然后重定向到主页。后续每个请求都从 Session 里取用户信息取不到就跳回登录页。代码结构一般分三层Servlet控制层、Service业务层、Dao数据访问层。登录的 Servlet 核心代码WebServlet(/login) public class LoginServlet extends HttpServlet { private UserService userService new UserServiceImpl(); Override protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException { String username req.getParameter(username); String password req.getParameter(password); // 密码一般做 MD5 或 BCrypt 加密后比对 User user userService.login(username, password); if (user ! null) { HttpSession session req.getSession(); session.setAttribute(currentUser, user); resp.sendRedirect(req.getContextPath() /index.jsp); } else { req.setAttribute(msg, 用户名或密码错误); req.getRequestDispatcher(/login.jsp).forward(req, resp); } } }这里有几个参数和逻辑要说明。req.getParameter 拿到的都是字符串如果前端传了空值要在 Service 层做非空校验。session.setAttribute 存的 key 用 currentUser 还是 user 无所谓但整个项目要统一否则后面取的时候对不上。resp.sendRedirect 和 req.getRequestDispatcher 的区别是前者是客户端跳转地址栏会变后者是服务器端转发地址栏不变。登录成功用重定向防止刷新页面重复提交登录失败用转发保留请求域里的错误信息。光有登录还不够得配一个过滤器拦截所有需要登录才能访问的页面。过滤器里判断 Session 里有没有 currentUser没有就跳登录页。注意放行登录页、注册页、静态资源css、js、图片否则会死循环。WebFilter(/*) public class LoginFilter implements Filter { Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException { HttpServletRequest req (HttpServletRequest) request; HttpServletResponse resp (HttpServletResponse) response; String uri req.getRequestURI(); // 放行登录、注册、静态资源 if (uri.contains(/login) || uri.contains(/register) || uri.contains(/css/) || uri.contains(/js/) || uri.contains(/images/)) { chain.doFilter(request, response); return; } HttpSession session req.getSession(); if (session.getAttribute(currentUser) null) { resp.sendRedirect(req.getContextPath() /login.jsp); return; } chain.doFilter(request, response); } }过滤器的 urlPattern 写 / 表示拦截所有请求然后在代码里手动放行。另一种做法是只拦截特定路径比如 /admin/但毕设项目一般所有页面都要登录所以用 /* 更省事。注意放行规则里用 contains 判断 URI别用 equals因为请求可能带参数。3.2 商品管理与分页查询SQL 拼接和参数传递商品管理是超市系统的核心模块包含列表展示、新增、修改、删除、搜索。列表展示一定要分页否则数据一多页面直接卡死。分页查询的核心是两条 SQL一条查总数一条查当前页数据。public PageBeanProduct findByPage(int currentPage, int pageSize, String keyword) { PageBeanProduct pageBean new PageBean(); // 第一条查总数 String countSql SELECT COUNT(*) FROM product WHERE product_name LIKE ?; int totalCount productDao.count(countSql, % keyword %); // 第二条查当前页数据 String listSql SELECT * FROM product WHERE product_name LIKE ? LIMIT ?, ?; int start (currentPage - 1) * pageSize; ListProduct list productDao.findList(listSql, % keyword %, start, pageSize); pageBean.setTotalCount(totalCount); pageBean.setPageSize(pageSize); pageBean.setCurrentPage(currentPage); pageBean.setList(list); // 计算总页数 int totalPage (int) Math.ceil((double) totalCount / pageSize); pageBean.setTotalPage(totalPage); return pageBean; }参数说明currentPage 是当前页码从 1 开始pageSize 是每页条数一般设 10 或 15start 是 LIMIT 的第一个参数表示偏移量计算公式是 (currentPage - 1) * pageSize。keyword 是搜索关键词用 LIKE 模糊匹配前后加 %。注意 LIMIT 后面两个参数第一个是偏移量第二个是每页条数别写反了。PageBean 是一个封装类包含 totalCount总记录数、totalPage总页数、currentPage当前页、pageSize每页条数、list当前页数据。前端 JSP 页面根据这些字段渲染分页条上一页、下一页、页码列表。这里有个容易翻车的地方SQL 拼接。如果用 Statement 而不是 PreparedStatement把 keyword 直接拼进 SQL会有 SQL 注入风险。比如用户输入 OR 11整个查询条件就失效了。必须用 PreparedStatement参数用 ? 占位然后 setString、setInt 依次赋值。public ListProduct findList(String sql, Object... params) { ListProduct list new ArrayList(); try (Connection conn DruidUtils.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { for (int i 0; i params.length; i) { ps.setObject(i 1, params[i]); } try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Product p new Product(); p.setProductId(rs.getInt(product_id)); p.setProductName(rs.getString(product_name)); p.setSalePrice(rs.getBigDecimal(sale_price)); p.setStock(rs.getInt(stock)); list.add(p); } } } catch (SQLException e) { throw new RuntimeException(查询商品列表失败, e); } return list; }这段代码用可变参数 Object... params 来适配不同数量的占位符setObject 会自动处理类型。ResultSet 取值时getInt、getString、getBigDecimal 要和数据库字段类型对应。商品价格用 BigDecimal 而不是 double避免浮点数精度问题这是血泪经验用 double 算钱迟早出问题。3.3 库存扣减与事务控制别让超卖发生在你的系统里库存扣减是超市系统里最需要小心的地方。假设两个收银员同时卖同一件商品当前库存是 1两人都查到库存为 1都判断可以卖然后都执行扣减最后库存变成 -1。这就是典型的超卖问题。解决办法有两种一种是在 SQL 层面加条件扣减时判断库存是否足够另一种是用事务加锁。毕设项目推荐第一种简单可靠。UPDATE product SET stock stock - ? WHERE product_id ? AND stock ?;这条 SQL 的意思是只有当库存大于等于购买数量时才执行扣减。执行后检查受影响行数如果返回 0说明库存不足回滚事务。public boolean reduceStock(int productId, int quantity) { String sql UPDATE product SET stock stock - ? WHERE product_id ? AND stock ?; try (Connection conn DruidUtils.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, quantity); ps.setInt(2, productId); ps.setInt(3, quantity); int rows ps.executeUpdate(); return rows 0; } catch (SQLException e) { throw new RuntimeException(库存扣减失败, e); } }但光扣库存还不够销售订单和销售明细也要同时写入。这三步操作必须在一个事务里要么全成功要么全回滚。手动管理事务的代码结构Connection conn null; try { conn DruidUtils.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 插入销售订单 saleOrderDao.insert(conn, order); // 2. 插入销售明细 for (SaleDetail detail : detailList) { saleDetailDao.insert(conn, detail); // 3. 扣减库存 boolean success productDao.reduceStock(conn, detail.getProductId(), detail.getQuantity()); if (!success) { throw new RuntimeException(库存不足 detail.getProductId()); } } conn.commit(); // 提交事务 } catch (Exception e) { if (conn ! null) { conn.rollback(); // 回滚事务 } throw new RuntimeException(下单失败, e); } finally { if (conn ! null) { conn.setAutoCommit(true); conn.close(); } }关键点setAutoCommit(false) 开启事务后所有 Dao 方法必须共用同一个 Connection不能各自去连接池拿新连接否则事务不生效。这就是为什么 Dao 方法要接收 Connection 参数。commit 提交后数据才真正写入rollback 回滚后所有操作撤销。finally 里恢复自动提交并关闭连接防止连接池泄漏。4. 避坑与排查那些让答辩现场尴尬的常见问题4.1 中文乱码从请求到响应全链路排查现象表单提交中文数据库里存的是问号或者数据库里正常页面显示乱码。原因字符集不统一。请求端、Tomcat、数据库连接、数据库表、响应端任何一处编码不一致都会乱码。解决按顺序检查。第一JSP 页面顶部加% page contentTypetext/html;charsetUTF-8 languagejava %。第二Servlet 里在 doGet/doPost 第一行加req.setCharacterEncoding(UTF-8); resp.setContentType(text/html;charsetUTF-8);。第三数据库连接串加useUnicodetruecharacterEncodingutf8。第四数据库和表创建时指定 utf8mb4。第五Tomcat 的 server.xml 里 Connector 加URIEncodingUTF-8。五处都对了乱码基本消失。4.2 数据库连接报错驱动版本和时区问题现象启动 Tomcat 报No suitable driver found或者The server time zone value ?D1ú±ê×?ê±?? is unrecognized。原因第一种是 MySQL 驱动 jar 没放进 WEB-INF/lib第二种是 MySQL 8 的时区没配。解决驱动问题在 IDEA 的 Artifacts 里把 mysql-connector-java 的 jar 加到 WEB-INF/lib 下。时区问题连接串加serverTimezoneAsia/Shanghai。另外 MySQL 8 的驱动类名是com.mysql.cj.jdbc.Driver不是com.mysql.jdbc.Driver写错了也会报错。4.3 分页查询页码越界空数据和最后一页的处理现象点下一页点到最后一页之后再点页面空白或者报错。原因currentPage 超过了 totalPageSQL 的 LIMIT 偏移量超出总记录数返回空列表。解决在 Service 层做边界判断。如果 currentPage 1设为 1如果 currentPage totalPage设为 totalPage。另外如果 totalCount 为 0totalPage 要设为 1避免除零或者页码显示异常。if (currentPage 1) currentPage 1; if (totalPage 0) totalPage 1; if (currentPage totalPage) currentPage totalPage;4.4 Session 失效与重复登录过滤器里的放行规则现象登录后点几个页面就跳回登录页或者退出登录后还能访问内部页面。原因Session 超时时间太短或者过滤器放行规则写错了把不该放行的路径放行了。解决在 web.xml 里配置 session-timeout单位是分钟一般设 30。过滤器放行规则用精确匹配或者 startsWith别用 contains 匹配 /login因为 /loginCheck 也会被放行。退出登录时调用session.invalidate()销毁 Session然后重定向到登录页。4.5 库存扣减为负数并发场景下的数据一致性现象两个人同时下单库存扣成了负数。原因先查库存再扣减两步之间有时间窗口并发时都查到足够库存。解决用UPDATE product SET stock stock - ? WHERE product_id ? AND stock ?把判断和扣减合并成一条原子操作。执行后检查executeUpdate()的返回值如果为 0说明库存不足抛异常回滚事务。这是最简单有效的防超卖方案不需要额外的锁。5. 从能跑到能讲答辩前把系统拆一遍的笨办法答辩前一周别再去加新功能了。把系统里最核心的三条链路——登录、商品管理、下单——从头到尾手动走一遍每走一步问自己三个问题这个请求从浏览器发出经过哪些层最后落到哪张表如果这一步失败了错误信息会在哪里出现怎么排查如果老师问“为什么这么设计”我能不能说出两个以上的理由我自己的习惯是打开 IDEA 的 Debug 模式在登录的 Servlet 里打断点然后一步步 F8 往下走看 request 里的参数、session 里的值、数据库返回的结果。走完一遍整个链路的细节就刻在脑子里了。比背代码管用。还有一个笨办法把数据库脚本里的建表语句抄一遍每张表旁边写上“这张表在哪个页面用到了哪个 Servlet 操作它”。抄完你会发现有些表从头到尾没被用过有些表被多个模块共用。没用的表删掉论文里别写共用的表想清楚为什么共用这是论文里“数据库设计”章节的素材。最后准备一个“翻车清单”。把你开发过程中遇到的所有报错、乱码、数据对不上的问题按“现象→原因→解决”写下来。答辩时老师问“你遇到的最大困难是什么”你直接从这个清单里挑一个讲比临时编靠谱得多。我当年就是靠这个清单把老师问的“库存超卖怎么解决”答得明明白白。希望帮到你。本文还有配套的精品资源点击获取
返回列表