简介:这份资源面向Java初学者与课程设计开发者,提供一套基于Java与SQL Server的超市管理系统完整实现,可用于毕业设计、课程实训或后台管理系统的入门参考。系统覆盖商品管理、库存控制、销售记录、会员管理与报表生成等模块,技术栈涉及JDBC连接数据库、Servlet与JSP构建MVC结构、Spring管理Bean与事务、MyBatis封装持久层操作,数据库侧包含表结构设计、索引优化、存储过程与触发器等实践要点。压缩包为rar格式,大小约2.16MB,上游未提供文件明细,具体文件构成以实际下载为准。目前已有492人学习下载,说明该案例在同类教学资源中具有一定参考价值。读者可从中了解Java Web项目与SQL Server配合的典型分层方式,对照业务模块理解数据表设计与SQL优化思路,并借鉴系统开发从需求分析到部署上线的完整流程,适合作为动手实践与查漏补缺的参考素材。
1. 从一份能跑起来的超市管理系统源码说起
接手一套别人写了一半的超市管理系统,最怕的不是业务复杂,而是打开压缩包发现只有一堆.java文件,数据库脚本缺失、依赖版本对不上、连登录入口在哪都要靠猜。这次拆的这份 Java + SQL Server 超市管理系统源码,恰好是少数能直接跑通的一套:商品管理、库存出入库、收银结算、会员积分、销售统计几个核心模块都在,前端是 Swing 桌面端,后端用 JDBC 直连 SQL Server,没有堆一堆用不上的框架。它适合两类人——课程设计要交差、想找个结构完整又不臃肿的参考实现的新手,以及需要一套能改造成小型门店收银工具的老手。下面按「先跑通、再拆结构、最后改业务」的顺序,把这份资源从环境搭建到二次开发讲透,坑点一个不落。
2. 环境搭建与数据库初始化:SQL Server 连接串和建库脚本怎么配
拿到源码第一步不是急着点运行,而是把 SQL Server 这一侧先立住。这套系统对数据库的依赖很重,商品、库存、订单、会员四张主表加若干关联表,任何一张建错字段类型,后面 JDBC 查询就会在ResultSet.getXXX上翻车。我一般按「装库 → 建库 → 导脚本 → 验连接」四步走,每步都能单独验证,出问题好定位。
2.1 SQL Server 版本选择与实例配置
源码里的连接串默认走的是localhost:1433,说明作者用的是默认实例加 TCP 端口。实际部署时有两个常见选择:Express 版够用但默认不开 TCP/IP,需要手动在配置管理器里启用;Developer 版功能全、免费用于开发,装完默认实例名就是机器名。我倾向后者,省掉改实例名的麻烦。
装完之后必须确认三件事,缺一个后面就连不上:
- SQL Server Browser 服务是否启动(命名实例才需要,默认实例可跳过)
- TCP/IP 协议是否启用,端口是否为 1433
- 是否开启了「SQL Server 和 Windows 身份验证模式」,纯 Windows 认证模式下 JDBC 用账号密码登录会直接被拒
第三点是最容易漏的。很多人装库时一路下一步,默认就是仅 Windows 认证,结果 Java 端报Login failed for user 'sa',还以为是密码错了。改法是在 SSMS 里右键实例 → 属性 → 安全性 → 改成混合模式,然后重启服务,再启用 sa 账号并设密码。
2.2 建库与脚本导入的实操步骤
源码包里的 SQL 脚本一般叫db_supermarket.sql或类似名字,里面包含建库、建表、插初始数据三部分。导入时不要用图形界面的「导入数据」向导,那个对含GO分隔符的脚本支持很差,容易只执行了第一段。正确做法是新建查询窗口,把脚本内容贴进去整体执行,或者用命令行:
# 用 sqlcmd 执行建库脚本,-S 指定实例,-U/-P 指定账号密码 sqlcmd -S localhost -E -i db_supermarket.sql # 如果走 SQL 认证: sqlcmd -S localhost -U sa -P YourPassword -i db_supermarket.sql-E表示用当前 Windows 身份登录,本地开发最省事;-i后面跟脚本路径。执行完用下面这条确认表都建出来了:
-- 切到目标库,列出所有用户表 USE supermarket; GO SELECT name FROM sys.tables ORDER BY name;正常应该能看到商品表、库存表、订单主表、订单明细表、会员表、用户表这几张。如果只有一两张,多半是脚本里的GO被某个客户端吞了,换 sqlcmd 重跑一遍即可。
2.3 JDBC 连接串的四个关键参数
Java 侧连 SQL Server 用的是微软官方驱动mssql-jdbc,连接串长这样:
// 典型连接串,各参数含义见下方说明 String url = "jdbc:sqlserver://localhost:1433;" + "databaseName=supermarket;" + "encrypt=false;" + "trustServerCertificate=true;" + "loginTimeout=5;"; Connection conn = DriverManager.getConnection(url, "sa", "YourPassword");四个参数值得单独说。databaseName必须和建库脚本里的库名完全一致,大小写不敏感但拼错就是Cannot open database。encrypt=false是给老版本驱动留的兼容项,新版驱动默认要求加密,本地没配证书时会报 SSL 握手失败,关掉最省事。trustServerCertificate=true配合上一条使用,避免自签证书校验。loginTimeout=5是我自己加的,默认超时太长,连不上时界面会卡死十几秒,设成 5 秒能快速暴露配置错误。
驱动 jar 包要放进项目的lib目录并加到 classpath,Maven 项目则加依赖:
<dependency> <groupId>com.microsoft.sqlserver</groupId> <artifactId>mssql-jdbc</artifactId> <version>12.4.2.jre11</version> </dependency>版本号按你的 JDK 选,jre8和jre11后缀别搞混,用错会在启动时报UnsupportedClassVersionError。
3. 源码结构拆解:从登录入口到收银结算的调用链
环境通了之后,别急着改业务,先把调用链摸清楚。这套系统的分层不算严格,但胜在直白:ui包放 Swing 窗体,dao包放 JDBC 操作,model包放实体类,util包放数据库连接和工具方法。理解这条链,后面加字段、改逻辑才知道动哪里。
3.1 包结构与启动类定位
打开源码先找带main方法的类,通常在com.xxx.supermarket根包下,名字类似Main或LoginFrame。启动流程是:加载登录窗体 → 校验用户表 → 进主界面 → 按角色加载菜单。用户表里一般有role字段区分管理员和收银员,管理员能看到商品管理和统计,收银员只能进收银界面。这个权限控制是硬编码在菜单初始化里的,不是配置化的,改角色要动代码。
DAO 层的写法是典型的「一个实体一个类」,比如GoodsDao里有queryAll、insert、updateStock这些方法,每个方法内部自己拿连接、拼 SQL、关资源。没有连接池,每次操作新建Connection,小数据量下没问题,并发一高就明显卡。这一点后面避坑章节会细说。
3.2 商品与库存模块的数据流
商品模块的核心是「商品表 + 库存表」两张表联动。新增商品时先插商品表拿到自增主键,再往库存表插一条初始库存记录,两步在同一个事务里。源码里这段事务处理写得比较糙,用的是手动setAutoCommit(false)加 try-catch 回滚,没有用 try-with-resources,异常路径下连接可能没关干净。我一般会重写成这样:
// 新增商品并初始化库存,放在同一事务里保证一致性 public boolean addGoods(Goods g, int initStock) { String sql1 = "INSERT INTO goods(name, price, category) VALUES(?,?,?)"; String sql2 = "INSERT INTO stock(goods_id, quantity) VALUES(?,?)"; try (Connection conn = DBUtil.getConnection()) { conn.setAutoCommit(false); // 关闭自动提交,开启事务 try (PreparedStatement ps1 = conn.prepareStatement(sql1, Statement.RETURN_GENERATED_KEYS)) { ps1.setString(1, g.getName()); ps1.setBigDecimal(2, g.getPrice()); ps1.setString(3, g.getCategory()); ps1.executeUpdate(); ResultSet keys = ps1.getGeneratedKeys(); // 取回自增主键 keys.next(); int goodsId = keys.getInt(1); try (PreparedStatement ps2 = conn.prepareStatement(sql2)) { ps2.setInt(1, goodsId); ps2.setInt(2, initStock); ps2.executeUpdate(); } conn.commit(); // 两步都成功才提交 return true; } catch (SQLException e) { conn.rollback(); // 任一步失败整体回滚 throw e; } } catch (SQLException e) { e.printStackTrace(); return false; } }RETURN_GENERATED_KEYS是关键参数,没有它拿不到刚插入商品的自增 ID,库存表就没法关联。setBigDecimal对应价格字段,别用setDouble,金额用浮点会出精度问题,这是收银系统的老坑。
3.3 收银结算的事务边界
收银是整个系统里唯一必须严格保证原子性的操作:扣库存、写订单主表、写订单明细、加会员积分,四步要么全成要么全败。源码里这段逻辑分散在CashierFrame的事件处理方法里,SQL 直接写在界面类中,事务边界不清晰。改造时我建议把这段抽到OrderService里,用显式事务包住:
// 结算核心:扣库存 + 写订单 + 加积分,一个事务内完成 public boolean checkout(List<CartItem> items, int memberId, BigDecimal total) { String deduct = "UPDATE stock SET quantity = quantity - ? WHERE goods_id = ? AND quantity >= ?"; String order = "INSERT INTO orders(member_id, total, create_time) VALUES(?,?,GETDATE())"; String detail = "INSERT INTO order_detail(order_id, goods_id, qty, price) VALUES(?,?,?,?)"; String point = "UPDATE member SET points = points + ? WHERE id = ?"; try (Connection conn = DBUtil.getConnection()) { conn.setAutoCommit(false); try { int orderId; try (PreparedStatement ps = conn.prepareStatement(order, Statement.RETURN_GENERATED_KEYS)) { ps.setInt(1, memberId); ps.setBigDecimal(2, total); ps.executeUpdate(); ResultSet rs = ps.getGeneratedKeys(); rs.next(); orderId = rs.getInt(1); } for (CartItem it : items) { // 扣库存时带 quantity >= ? 条件,防止超卖 try (PreparedStatement ps = conn.prepareStatement(deduct)) { ps.setInt(1, it.getQty()); ps.setInt(2, it.getGoodsId()); ps.setInt(3, it.getQty()); if (ps.executeUpdate() == 0) { // 影响行数为 0 说明库存不足 conn.rollback(); return false; } } try (PreparedStatement ps = conn.prepareStatement(detail)) { ps.setInt(1, orderId); ps.setInt(2, it.getGoodsId()); ps.setInt(3, it.getQty()); ps.setBigDecimal(4, it.getPrice()); ps.executeUpdate(); } } try (PreparedStatement ps = conn.prepareStatement(point)) { ps.setInt(1, total.intValue()); // 简化:1 元 1 分 ps.setInt(2, memberId); ps.executeUpdate(); } conn.commit(); return true; } catch (SQLException e) { conn.rollback(); throw e; } } catch (SQLException e) { e.printStackTrace(); return false; } }UPDATE ... WHERE quantity >= ?这个写法是防超卖的核心,靠数据库行锁保证并发下不会扣成负数,比先查再扣的两步操作可靠得多。GETDATE()是 SQL Server 的内置函数,取服务器当前时间,比 Java 端传时间更不容易受客户端时钟影响。
4. 避坑与排查:五个让系统跑不起来的真实问题
这套源码能跑,但「能跑」和「跑得稳」之间隔着不少坑。下面五条是我实际部署和改造时踩过的,按现象、原因、解决三段写,照着排查能省不少时间。
4.1 中文乱码:商品名存进去变成问号
现象:新增商品时中文名称在界面显示正常,存进数据库再查出来变成???。原因有两层,一是 SQL Server 的varchar字段不认 Unicode,中文必须用nvarchar;二是 JDBC 连接串没指定字符集,驱动按平台默认编码处理。解决:建表脚本里所有存文本的字段改成nvarchar,连接串加sendStringParametersAsUnicode=true(新版驱动默认就是 true,老驱动要显式加)。改完记得把已有数据清掉重插,乱码数据改字段类型也救不回来。
4.2 驱动版本与 JDK 不匹配导致启动即崩
现象:双击启动脚本,控制台一闪而过,日志里是UnsupportedClassVersionError。原因:mssql-jdbc的 jar 包后缀标了jre8和jre11,用 JDK 8 跑jre11的包就会报这个错。解决:先java -version确认 JDK 版本,再选对应后缀的驱动。顺带一提,JDK 8 下用jre8后缀的 12.x 版本驱动没问题,但别用太老的 6.x,那个版本对 TLS 支持有缺陷,连新版 SQL Server 会握手失败。
4.3 连接未关闭导致连接数耗尽
现象:系统用一段时间后所有操作都卡住,重启才好。原因:DAO 层大量Connection、PreparedStatement、ResultSet没有在 finally 里关闭,异常路径下更是直接泄漏。SQL Server Express 默认连接数有限,泄漏几十个就满了。解决:全部改成 try-with-resources 写法,资源在括号里声明,方法结束自动关。改造时重点查catch块里有没有漏关,那是泄漏高发区。
4.4 金额用 double 导致对账差几分钱
现象:收银小票合计和数据库订单总额对不上,差 0.01。原因:价格字段在 Java 端用了double,累加时浮点误差累积。解决:实体类价格字段改BigDecimal,数据库字段用decimal(10,2),所有加减用BigDecimal.add、multiply,别用+和*。BigDecimal构造要用字符串或valueOf,别用new BigDecimal(0.1),那个一样有误差。
4.5 自增主键取回失败导致库存关联为空
现象:新增商品成功,但库存表里goods_id是 0 或 null。原因:prepareStatement时没传Statement.RETURN_GENERATED_KEYS,getGeneratedKeys()返回空结果集,代码里rs.next()没判断就直接getInt,拿到默认值 0。解决:按 3.2 节的写法补上参数,并在rs.next()前加判断,取不到主键就抛异常回滚,别让脏数据落库。
5. 二次开发与验证:把系统改成能用的门店工具
跑通只是起点,真正有价值的是按自己场景改。这套系统的扩展点集中在三处:商品分类、会员规则、统计报表。改之前先想清楚验证方法,不然改完不知道对不对。
5.1 加一个商品分类维度的销售统计
原系统的销售统计只按时间汇总,加分类维度需要动 SQL 和界面两处。SQL 侧用JOIN把订单明细、商品、分类串起来:
-- 按分类统计销售额,日期范围用参数占位 SELECT c.name AS category, SUM(d.qty * d.price) AS amount, COUNT(DISTINCT o.id) AS order_count FROM order_detail d JOIN orders o ON o.id = d.order_id JOIN goods g ON g.id = d.goods_id JOIN category c ON c.id = g.category_id WHERE o.create_time BETWEEN ? AND ? GROUP BY c.name ORDER BY amount DESC;SUM(d.qty * d.price)用的是明细里的成交价而不是商品当前价,这点很重要,商品调价后历史订单金额不能跟着变。界面侧在统计窗体加一个JTable,把ResultSet逐行填进去即可,注意列名和 SQL 别名对齐。
5.2 用一组固定用例验证改造没改坏
每次改完业务,我都会跑一遍固定用例,比肉眼点界面靠谱。核心用例就四条:正常结算后库存减对、库存不足时结算被拒且订单不落库、会员积分按规则累加、统计金额和订单总额一致。前三条用代码直接调 Service 层验证,第四条用 SQL 对账:
-- 对账:订单主表总额应等于明细汇总 SELECT o.id, o.total, SUM(d.qty * d.price) AS calc FROM orders o JOIN order_detail d ON d.order_id = o.id GROUP BY o.id, o.total HAVING o.total <> SUM(d.qty * d.price);这条查出来有行就说明有对不上的订单,通常是事务没包全或者金额精度问题。我一般把它做成一个定时跑的校验,出问题早发现。
5.3 一个我坚持了很多年的习惯
这套系统我前后改过三版,最深的教训来自第一版:当时图快,直接在界面类里写 SQL,事务边界全靠记忆,结果一次并发测试把库存扣成了负数,查了两天才定位到是两笔结算交叉执行。从那以后我每次动收银相关的代码,都强制先把事务边界画出来,确认四步操作在同一个Connection的同一个事务里,再动手写。改完必跑 5.2 节那组用例,一条不过就不提交。这套笨办法救过我很多次,也希望帮到你。
本文还有配套的精品资源,点击获取