简介:这是一套基于JSP与Java技术栈的超市管理系统完整源码,采用B/S架构,配合MyEclipse与MySQL数据库开发,适合正在学习Java Web编程、需要课程设计或毕业设计参考的初学者与在校学生。系统覆盖人事、销售、进货、库存与客户五大管理模块,包含职工与供货商信息的增删改查、商品销售盘点、库存维护以及用户密码修改等功能,能够帮助读者理解JSP页面与后台逻辑的协作方式。压缩包为rar格式,大小约1.06MB,内含完整源代码与数据库脚本,数据库文件为jspsupermarket.sql,管理员账号密码均为admin,开发环境为Tomcat7.0、MyEclipse8.5与MySQL5.0。目前已有1024人学习浏览,读者可据此快速搭建本地运行环境,对照源码梳理模块划分与数据库表结构,掌握B/S模式下Java Web项目的开发流程与调试思路。
1. JSP超市管理系统:一套还在被反复检索的 JavaWeb 骨架
打开招聘网站搜「Java 实习」,十有八九能看到「熟悉 JSP/Servlet、MySQL 者优先」。很多同学的第一反应是:JSP 不是早就过时了吗?可现实是,大量中小企业的进销存、超市收银、库存台账系统,底层仍然是 JSP + Servlet + MySQL 这套组合,跑在 MyEclipse 里,部署在 Tomcat 上。原因很朴素——开发快、部署轻、维护的人好找。这套「JSP超市管理系统」正是这类项目的典型代表:商品管理、库存出入库、收银结算、会员积分、销售统计,一个都不少。
它解决的核心问题是把线下超市的纸质台账搬到 Web 端,让收银员、库管、店长在浏览器里各干各的活。适合谁?适合正在做课程设计、毕业设计的学生,也适合刚转 JavaWeb、想拿一个完整项目练手的初级工程师。下面我不讲空话,直接按「环境怎么搭 → 表怎么设计 → 功能怎么落地 → 坑在哪」这条线,把整套东西拆开讲清楚,你照着敲就能跑起来。
2. 环境与工程骨架:MyEclipse + Tomcat + MySQL 的最小可跑组合
2.1 为什么这套老技术栈还值得动手
先说选型理由,不然你敲到一半会怀疑人生。JSP 的本质是「在 HTML 里写 Java」,服务端渲染,浏览器拿到的是最终 HTML,不需要前端框架那一套构建工具。对超市这种表单密集、页面跳转多的系统,JSP 的开发效率其实不低。MyEclipse 虽然界面老旧,但它对 JSP 的语法提示、Tomcat 集成、断点调试做得相当顺手,尤其是自动补全request.getParameter这类高频代码,能省不少事。
MySQL 选 5.7 还是 8.0?我的血泪经验是:如果学校机房或老服务器上装的是 5.7,就统一用 5.7,别混用。8.0 默认的caching_sha2_password认证插件会让老版本 JDBC 驱动直接连不上,报Unable to load authentication plugin。新手最容易在这里翻车。JDBC 驱动用mysql-connector-java-5.1.47配 5.7,或者8.0.28配 8.0,版本对齐是第一原则。
工程结构上,标准做法是src下分dao、service、servlet、entity四层,WebRoot下放jsp页面和WEB-INF/lib依赖。别把所有 Servlet 堆在一个包里,后期改一个功能要找半天。
2.2 建库建表:五张核心表撑起整个系统
超市系统的数据模型不复杂,抓住「商品—库存—订单—会员—用户」这条主线就够了。下面是我一般会用的建表脚本,字段做了精简但保留了关键约束。
-- 商品表:超市的核心资产 CREATE TABLE product ( id INT PRIMARY KEY AUTO_INCREMENT, pname VARCHAR(100) NOT NULL COMMENT '商品名称', category VARCHAR(50) COMMENT '分类', price DECIMAL(10,2) NOT NULL DEFAULT 0.00 COMMENT '售价', stock INT NOT NULL DEFAULT 0 COMMENT '库存数量', barcode VARCHAR(50) UNIQUE COMMENT '条形码', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 会员表:积分和折扣挂钩 CREATE TABLE member ( id INT PRIMARY KEY AUTO_INCREMENT, phone VARCHAR(20) UNIQUE NOT NULL, mname VARCHAR(50), points INT DEFAULT 0 COMMENT '积分', level TINYINT DEFAULT 1 COMMENT '1普通 2银卡 3金卡' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单主表 CREATE TABLE orders ( id INT PRIMARY KEY AUTO_INCREMENT, order_no VARCHAR(32) UNIQUE NOT NULL, member_id INT, total DECIMAL(10,2), pay_type TINYINT COMMENT '1现金 2扫码 3会员卡', create_time DATETIME DEFAULT CURRENT_TIMESTAMP ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 订单明细表 CREATE TABLE order_item ( id INT PRIMARY KEY AUTO_INCREMENT, order_id INT NOT NULL, product_id INT NOT NULL, num INT NOT NULL, subtotal DECIMAL(10,2), INDEX idx_order (order_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; -- 系统用户表 CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) UNIQUE NOT NULL, password VARCHAR(64) NOT NULL, role TINYINT DEFAULT 1 COMMENT '1收银员 2库管 3管理员' ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;逻辑说明:product表的stock字段是并发扣减的关键,后面会讲怎么防超卖。order_item单独建表而不是塞 JSON,是为了销售统计时能直接GROUP BY product_id。barcode加唯一索引,避免同一条码重复录入。字符集统一utf8mb4,不然商品名里的生僻字或 emoji 会变问号。
参数说明:DECIMAL(10,2)表示最多 10 位、2 位小数,金额别用FLOAT,浮点误差在收银场景是致命的。ENGINE=InnoDB必须写,MyISAM 不支持事务,扣库存和生成订单没法保证一致性。
2.3 在 MyEclipse 里把工程跑起来
步骤一:新建 Web Project,命名SuperMarket,JRE 选 1.8,不要选 11 以上,老版本 Tomcat 对高版本字节码支持不好。步骤二:把mysql-connector-java的 jar 丢进WebRoot/WEB-INF/lib,右键 Build Path 确认已加入。步骤三:配置数据库连接工具类。
// DBUtil.java —— 全项目唯一的连接入口 public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/supermarket?useUnicode=true&characterEncoding=utf8&useSSL=false"; private static final String USER = "root"; private static final String PWD = "你的密码"; static { try { Class.forName("com.mysql.jdbc.Driver"); // 5.1.x 驱动写法 } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConn() throws SQLException { return DriverManager.getConnection(URL, USER, PWD); } public static void close(Connection c, Statement s, ResultSet r) { try { if (r != null) r.close(); } catch (Exception e) {} try { if (s != null) s.close(); } catch (Exception e) {} try { if (c != null) c.close(); } catch (Exception e) {} } }逻辑说明:useSSL=false在本地开发时关掉 SSL 握手,避免控制台刷一堆警告。characterEncoding=utf8保证中文不乱码。Class.forName放在静态块里只加载一次。参数说明:如果你用的是 8.0 驱动,类名要改成com.mysql.cj.jdbc.Driver,URL 里加serverTimezone=Asia/Shanghai,否则会报时区错误。
步骤四:右键项目 → MyEclipse → Add and Remove Deployments,把项目部署到 Tomcat,启动后访问http://localhost:8080/SuperMarket/login.jsp。能看到登录页,环境就算通了。
3. 核心功能落地:从登录到收银的完整链路
3.1 登录与权限过滤:一个 Filter 挡住所有越权
登录本身不难,难的是别让收银员直接敲 URL 进管理员页面。常见做法是写一个LoginFilter,拦截所有/*,放行登录页和静态资源。
// LoginFilter.java 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("/css/") || uri.contains("/js/")) { chain.doFilter(req, resp); return; } Object user = request.getSession().getAttribute("loginUser"); if (user == null) { response.sendRedirect(request.getContextPath() + "/login.jsp"); } else { chain.doFilter(req, resp); } } }逻辑说明:getSession().getAttribute("loginUser")是判断登录态的唯一依据,登录成功后往 session 里塞这个 key。参数说明:web.xml里配置 Filter 时<url-pattern>/*</url-pattern>,<dispatcher>REQUEST</dispatcher>即可,别配 FORWARD,否则转发到 JSP 时会被二次拦截导致死循环。
权限细分的话,在 Filter 里再读sys_user.role,管理员才放行/admin/路径。这里有个坑:role 存在 session 里比每次查库快,但用户被改权限后要重新登录才生效,小系统可以接受。
3.2 商品管理与分页:别一次性把整张表查出来
商品列表是最容易写崩的地方。新手常写SELECT * FROM product然后全塞进 List 丢给 JSP,商品上千条时页面直接卡死。正确做法是分页查询。
// ProductDao.java 分页查询 public List<Product> findByPage(int page, int size, String keyword) { List<Product> list = new ArrayList<>(); String sql = "SELECT * FROM product WHERE pname LIKE ? LIMIT ?, ?"; try (Connection c = DBUtil.getConn(); PreparedStatement ps = c.prepareStatement(sql)) { ps.setString(1, "%" + (keyword == null ? "" : keyword) + "%"); ps.setInt(2, (page - 1) * size); // 起始偏移量 ps.setInt(3, size); // 每页条数 ResultSet rs = ps.executeQuery(); while (rs.next()) { Product p = new Product(); p.setId(rs.getInt("id")); p.setPname(rs.getString("pname")); p.setPrice(rs.getBigDecimal("price")); p.setStock(rs.getInt("stock")); list.add(p); } } catch (SQLException e) { e.printStackTrace(); } return list; }逻辑说明:LIMIT ?, ?第一个参数是偏移量(page-1)*size,第二个是每页条数。用PreparedStatement而不是字符串拼接,防 SQL 注入。参数说明:size一般设 10 或 15,看屏幕高度。keyword为空时拼%%相当于查全部,逻辑上没问题但要注意索引失效——LIKE '%xx%'用不上索引,商品量特别大时考虑全文索引或搜索引擎,小超市几千条无所谓。
JSP 页面用 JSTL 的<c:forEach>遍历,配合上一页/下一页按钮传page参数。别在 JSP 里写 Java 脚本片段查库,那是十年前的做法,现在至少用 EL 表达式。
3.3 收银结算:事务和库存扣减是命门
收银是整个系统最容易出事故的环节。用户点「结算」,背后要做三件事:生成订单主表、插入订单明细、扣减商品库存。这三步必须在一个事务里,否则订单生成了库存没扣,或者库存扣了订单没生成,账就对不上。
// OrderService.java 结算核心逻辑 public boolean checkout(List<CartItem> cart, int memberId, int payType) { Connection c = null; try { c = DBUtil.getConn(); c.setAutoCommit(false); // 开启事务 // 1. 生成订单号:时间戳 + 随机数 String orderNo = System.currentTimeMillis() + "" + (int)(Math.random()*9000+1000); BigDecimal total = BigDecimal.ZERO; for (CartItem item : cart) { total = total.add(item.getPrice().multiply(new BigDecimal(item.getNum()))); } // 2. 插入订单主表 String sql1 = "INSERT INTO orders(order_no, member_id, total, pay_type) VALUES(?,?,?,?)"; PreparedStatement ps1 = c.prepareStatement(sql1, Statement.RETURN_GENERATED_KEYS); ps1.setString(1, orderNo); ps1.setInt(2, memberId); ps1.setBigDecimal(3, total); ps1.setInt(4, payType); ps1.executeUpdate(); ResultSet keys = ps1.getGeneratedKeys(); keys.next(); int orderId = keys.getInt(1); // 3. 循环插入明细并扣库存 String sql2 = "INSERT INTO order_item(order_id, product_id, num, subtotal) VALUES(?,?,?,?)"; String sql3 = "UPDATE product SET stock = stock - ? WHERE id = ? AND stock >= ?"; for (CartItem item : cart) { PreparedStatement ps2 = c.prepareStatement(sql2); ps2.setInt(1, orderId); ps2.setInt(2, item.getProductId()); ps2.setInt(3, item.getNum()); ps2.setBigDecimal(4, item.getPrice().multiply(new BigDecimal(item.getNum()))); ps2.executeUpdate(); PreparedStatement ps3 = c.prepareStatement(sql3); ps3.setInt(1, item.getNum()); ps3.setInt(2, item.getProductId()); ps3.setInt(3, item.getNum()); // 关键:库存不足时影响行数为0 int rows = ps3.executeUpdate(); if (rows == 0) { c.rollback(); // 库存不够,整单回滚 return false; } } c.commit(); return true; } catch (SQLException e) { try { if (c != null) c.rollback(); } catch (SQLException ex) {} e.printStackTrace(); return false; } finally { try { if (c != null) { c.setAutoCommit(true); c.close(); } } catch (SQLException e) {} } }逻辑说明:setAutoCommit(false)开启事务,任何一步失败都rollback。扣库存的 SQL 用了AND stock >= ?,这是防超卖的关键——当库存不足时executeUpdate返回 0,直接回滚整单。参数说明:Statement.RETURN_GENERATED_KEYS用来拿自增主键orderId,不然明细表插不进去。订单号用时间戳加随机数,简单够用,高并发下理论上会撞,小超市一天几百单完全没问题。
会员积分在commit之后再更新,或者也放进事务里。放事务里更严谨,但要注意积分更新失败不能影响主流程,看业务取舍。
4. 避坑与排查:那些让我加班到凌晨的瞬间
4.1 中文乱码:三个地方都要设对
现象:商品名存进数据库变成???,或者页面显示乱码。原因:JSP 页面编码、请求编码、数据库连接编码三者不一致。解决:JSP 顶部写<%@ page contentType="text/html;charset=UTF-8" %>;Servlet 里第一行request.setCharacterEncoding("UTF-8");JDBC URL 加characterEncoding=utf8;数据库和表都用utf8mb4。四个地方缺一个都可能翻车,我一般写个检查清单挨个对。
4.2 Tomcat 启动报 404:部署路径和 context 对不上
现象:Tomcat 起来了,访问项目却 404。原因:MyEclipse 部署时 context root 默认是项目名,但有时会变成/或者带版本号。解决:在 Servers 视图双击 Tomcat,看 Modules 标签页里的 Path 是不是/SuperMarket。另外检查web.xml的welcome-file有没有配login.jsp,没配的话访问根路径找不到默认页也会 404。
4.3 数据库连接池耗尽:Connection 忘了关
现象:系统跑一段时间后报Too many connections。原因:getConn()拿到的连接用完没close(),尤其是异常分支里。解决:所有数据库操作放try-with-resources或者finally里关。上面DBUtil.close()就是干这个的。更彻底的做法是引入 Druid 或 C3P0 连接池,但小项目手动关也够用,关键是别漏。
4.4 金额计算精度丢失:用了 double 而不是 BigDecimal
现象:购物车合计 19.99 元,结算变成 19.989999。原因:double和float是二进制浮点,无法精确表示十进制小数。解决:所有金额字段用BigDecimal,数据库用DECIMAL。BigDecimal做加减乘用add、subtract、multiply,别用==比较,用compareTo。这个坑不踩一次记不住,踩一次记一辈子。
4.5 JSP 页面报Cannot resolve taglib:JSTL 包没导
现象:JSP 里写<c:forEach>标红,运行报找不到标签库。原因:JSTL 的 jar 没放进WEB-INF/lib。解决:下载jstl.jar和standard.jar(JSTL 1.2 只需一个jstl-1.2.jar),丢进 lib 目录,JSP 顶部加<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>。注意 URI 别写错,1.2 版本和 1.0 的 URI 不一样。
5. 进阶技巧:把销售统计和条码查询做扎实
系统能跑通只是及格线,真正拉开差距的是统计和查询。销售统计最常见的需求是「按天/按月看营业额」和「看单品销量排行」。前者用一条 SQL 就能出结果:
-- 按天统计营业额,近 7 天 SELECT DATE(create_time) AS day, SUM(total) AS amount, COUNT(*) AS orders FROM orders WHERE create_time >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY DATE(create_time) ORDER BY day DESC;逻辑说明:DATE()把 datetime 截成日期,DATE_SUB算时间范围,GROUP BY按天聚合。参数说明:INTERVAL 7 DAY改成1 MONTH就是按月,改成1 YEAR就是按年。这个查询走create_time索引才快,记得给这个字段加索引。
单品销量排行同理,order_item表GROUP BY product_id再JOIN product拿商品名,ORDER BY SUM(num) DESC LIMIT 10。这里有个细节:退款订单要不要算进去?如果系统有退货功能,orders表得加个status字段,统计时过滤掉已退款的。
条码查询是收银台的刚需——扫码枪扫一下,商品信息自动带出来。扫码枪本质是个键盘输入设备,扫完自动回车。前端监听输入框的keydown事件,回车时发 AJAX 请求后端SELECT * FROM product WHERE barcode = ?,拿到数据填进购物车。注意扫码枪输入速度极快,别用keyup逐字符查库,会打爆数据库,等回车一次性查。
最后说个验证方法:造 1000 条商品数据、跑 500 单结算,看统计结果和手工算的对不对得上。对不上就查事务有没有回滚干净、金额有没有用 BigDecimal。我自己的习惯是每次改完结算逻辑,先拿 3 件商品手动算一遍总价,再和系统输出比对,这个笨办法帮我挡掉过好几次精度 bug。希望帮到你。
本文还有配套的精品资源,点击获取