☰
Java Swing + SQL Server 超市管理系统课程设计复现与二次开发指南
2026/10/28 12:59:27 网站建设 项目流程

简介:这份Java超市管理系统实训报告面向计算机相关专业学生与Java初学者,服务于课程设计、数据库原理实践与项目答辩等场景,帮助读者完整走通从需求分析到系统测试的数据库应用开发流程。压缩包内共1个doc文档,约411KB,以报告正文形式组织,涵盖课程设计目的与意义、系统需求与可行性分析、开发技术及运行环境、系统结构框架与功能设计、数据库E-R模型与数据表设计等章节,并延伸至用户管理、商品管理、库存管理、销售记录等核心模块的实现思路与测试优化方法。该资源已有5778人学习下载,读者可借助其清晰的目录结构与规范的报告体例,快速理解超市管理系统的设计脉络,参考E-R图与表结构设计方法,并借鉴实训报告的写作框架与项目总结思路,适合作为课程设计参考与实战练手素材。

1. 从一份课程设计报告说起:这套超市管理系统到底能跑出什么

翻到一份十年前的课程设计报告,课题是超市管理系统,技术栈写着 Java + SQL Server 2005,报告里贴了建库语句、七张表结构、登录验证和商品录入的代码片段。很多人看到这种材料第一反应是"过时了",但真正拿它当二次开发起点的人会发现,它把进销存最核心的数据关系讲清楚了:职工、商品、供货商、进货、销售、库存,六张业务表加一张登录表,外键串起来就是一条完整的业务链。这份报告适合两类人:一是刚学完 Java 和数据库、想找一个能完整跑通的练手项目的新手;二是需要快速搭一个进销存原型、再往上加功能的开发者。它不追求界面花哨,胜在结构完整、逻辑直白,照着报告里的表设计和模块划分,能在一两天内把骨架立起来。

2. 技术选型与运行环境:为什么是 Java 桌面端加 SQL Server

2.1 报告里的技术栈拆解

报告正文写得很明确:前台开发工具 Eclipse,后台数据库 SQL Server 2005,运行环境 Windows XP,界面基于 Windows 图形用户界面。这里有个细节值得注意——报告第 3 章写"前台:Microsoft Visual Studio 2005",但第 6 章的代码却是 C# 风格的SqlDataReader、MessageBox.Show、ListViewItem,和标题里的"Java 程序设计"对不上。这是这类课程设计报告常见的拼接痕迹:文档模板和代码来源不是同一套。

对拿这份材料做二次开发的人来说,处理方式有两种。第一种是忠于标题,用 Java 重写:Swing 做界面,JDBC 连数据库,PreparedStatement替代字符串拼接。第二种是忠于代码,按 C# WinForms + ADO.NET 的路子走。我一般建议选第一种,因为标题和摘要都指向 Java,而且 Java 的生态在后续扩展上更顺手。下面所有代码示例都按 Java 路线给,表结构和业务逻辑完全沿用报告里的设计。

技术选型的合理性在于:这是一个单机、单用户、数据量在几千条量级的进销存系统,不需要 Web 容器,不需要分布式,桌面端加本地数据库是最省事的组合。用 Swing 而不是 JavaFX,是因为 Swing 在 JDK 里自带、零额外依赖,课程设计场景下启动成本最低。数据库用 SQL Server 是报告的历史选择,实际复现时换成 MySQL 或 PostgreSQL 也完全可行,SQL 语法差异很小,主要改连接串和驱动。

2.2 环境准备与依赖清单

按 Java 路线复现,需要准备的东西不多,但版本要对齐,否则会卡在一些莫名其妙的地方。

组件建议版本说明
JDK8 或 118 兼容性最好,Swing 组件行为稳定
数据库MySQL 5.7 / 8.0替代 SQL Server,语法改动小
JDBC 驱动mysql-connector-java 5.1.x 或 8.x与数据库版本匹配
IDEEclipse 或 IntelliJ IDEA报告用 Eclipse,IDEA 也行
构建工具无(手动加 jar)或 Maven小项目手动加 jar 更快

数据库连接部分,报告里用的是DataClass.MyMeans这种自定义类封装连接,Java 侧对应写一个DBUtil单例即可。常见做法是把连接串、用户名、密码放在一个db.properties里,避免硬编码。

// DBUtil.java —— 数据库连接工具类 import java.sql.*; import java.io.*; import java.util.Properties; public class DBUtil { private static String url; private static String user; private static String password; static { try (InputStream in = DBUtil.class.getClassLoader() .getResourceAsStream("db.properties")) { Properties p = new Properties(); p.load(in); url = p.getProperty("jdbc.url"); user = p.getProperty("jdbc.user"); password = p.getProperty("jdbc.password"); Class.forName(p.getProperty("jdbc.driver")); } catch (Exception e) { throw new RuntimeException("数据库配置加载失败", e); } } // 每次调用返回新连接,由调用方负责关闭 public static Connection getConnection() throws SQLException { return DriverManager.getConnection(url, user, password); } }

db.properties内容大致是jdbc.url=jdbc:mysql://localhost:3306/db_smms?useUnicode=true&characterEncoding=utf8、jdbc.user=root、jdbc.password=你的密码、jdbc.driver=com.mysql.jdbc.Driver(8.x 驱动写com.mysql.cj.jdbc.Driver)。这里的关键点是驱动类名和 URL 参数,characterEncoding=utf8不加的话中文商品名会变问号,这是血泪经验。

2.3 从报告到可运行工程的落地步骤

报告本身是文档,不是工程压缩包,所以第一步是把文档里的表结构翻译成建表脚本。报告第 5 章给了七张表的字段定义,直接照着写 DDL 即可。注意报告里库存表的建表语句有个明显错误:PRIMARY KEY (StockID)但字段列表里只有GoodsID和Amount,StockID根本没定义。复现时要补上这个字段,或者干脆用GoodsID做主键。

-- 建库 CREATE DATABASE db_smms DEFAULT CHARACTER SET utf8mb4; -- 职工信息表 CREATE TABLE staff_bill ( staff_id CHAR(6) PRIMARY KEY, name VARCHAR(8) NOT NULL, sex CHAR(2) NOT NULL, kind VARCHAR(10), id_card VARCHAR(18) NOT NULL, others VARCHAR(50) ); -- 用户登录表 CREATE TABLE users_info ( login_id CHAR(6) PRIMARY KEY, pass_word CHAR(6) NOT NULL ); -- 商品信息表 CREATE TABLE goods_info ( merch_id CHAR(6) PRIMARY KEY, proffer_id CHAR(6) NOT NULL, full_name VARCHAR(80) NOT NULL, kind VARCHAR(8), unit_cost DECIMAL(10,2) NOT NULL, others VARCHAR(50), CONSTRAINT fk_goods_sup FOREIGN KEY (proffer_id) REFERENCES sup_info(supplier_id) ); -- 供货商信息表 CREATE TABLE sup_info ( supplier_id CHAR(6) PRIMARY KEY, name VARCHAR(50) NOT NULL, contact_person VARCHAR(8) NOT NULL, phone VARCHAR(12) NOT NULL, address VARCHAR(50) NOT NULL, others VARCHAR(50) ); -- 进货信息表 CREATE TABLE buy ( buy_id CHAR(6) PRIMARY KEY, goods_id CHAR(6) NOT NULL, supplier_id CHAR(6) NOT NULL, price DECIMAL(10,2) NOT NULL, amount INT NOT NULL, sum DECIMAL(10,2) NOT NULL, date DATETIME NOT NULL, remark VARCHAR(50), CONSTRAINT fk_buy_goods FOREIGN KEY (goods_id) REFERENCES goods_info(merch_id), CONSTRAINT fk_buy_sup FOREIGN KEY (supplier_id) REFERENCES sup_info(supplier_id) ); -- 销售信息表 CREATE TABLE sell ( sell_id CHAR(6) PRIMARY KEY, goods_id CHAR(6) NOT NULL, amount INT NOT NULL, date DATETIME NOT NULL, CONSTRAINT fk_sell_goods FOREIGN KEY (goods_id) REFERENCES goods_info(merch_id) ); -- 库存信息表 CREATE TABLE stock ( goods_id CHAR(6) PRIMARY KEY, amount INT NOT NULL DEFAULT 0, CONSTRAINT fk_stock_goods FOREIGN KEY (goods_id) REFERENCES goods_info(merch_id) );

建表顺序有讲究:goods_info依赖sup_info,buy依赖goods_info和sup_info,所以sup_info必须最先建。报告里没强调这一点,直接按目录顺序执行会报外键错误。建完表插一条管理员账号,INSERT INTO users_info VALUES ('admin1', '123456'),就能进登录界面了。

3. 数据库设计与核心表关系:七张表撑起进销存闭环

3.1 E-R 关系与主外键链路

报告第 5 章给了 E-R 模型图,虽然图没贴出来,但从表结构能反推出关系:供货商一对多商品,商品一对多进货记录,商品一对多销售记录,商品一对一库存。职工表和登录表是独立的,不参与业务外键,只做权限和操作人记录。

这条链路的价值在于,任何一次进货或销售都能追溯到具体商品和供货商,库存表则是商品表的延伸——商品建档时同步插入一条库存记录,数量初始为 0。报告第 6 章商品添加代码里正好体现了这个设计:先insert into tb_Goods,再insert into tb_Stock,两条语句在同一个 try 块里,失败一起回滚(虽然原代码没显式开事务,这是个隐患,后面避坑章节会讲)。

3.2 库存视图与联表查询

报告 5.3 节给了一个库存视图v_StockSAS,把库存表和商品表内连接,输出商品名、库存量、备注。这个视图是库存管理界面的数据源,避免每次查询都手写 JOIN。

CREATE VIEW v_stock_sas AS SELECT s.goods_id, g.full_name AS goods_name, s.amount, g.others AS remark FROM stock s INNER JOIN goods_info g ON s.goods_id = g.merch_id;

视图的好处是界面层只认v_stock_sas这一个名字,后面如果库存表加字段,改视图定义就行,不用动 Java 代码。参数说明:s.amount是当前库存量,g.full_name是商品全名,g.others借用作备注。查询时直接SELECT * FROM v_stock_sas WHERE amount < 10就能筛出低库存商品,做补货提醒。

3.3 编号生成策略

报告代码里反复出现MyMC.GetAutocoding("tb_Goods", "GoodsID"),这是一个自动编号生成方法。课程设计里常见的做法是查当前表最大 ID,加一后补零到固定长度。Java 侧实现如下:

// 生成形如 G00001 的编号 public static String nextId(Connection conn, String table, String column, String prefix) throws SQLException { String sql = "SELECT MAX(" + column + ") FROM " + table; try (Statement st = conn.createStatement(); ResultSet rs = st.executeQuery(sql)) { if (rs.next()) { String max = rs.getString(1); if (max != null) { int n = Integer.parseInt(max.substring(prefix.length())) + 1; return prefix + String.format("%05d", n); } } } return prefix + "00001"; }

参数说明:table是表名,column是编号列,prefix是前缀(商品用 G、进货用 B、销售用 S)。这个方案在单机单用户场景够用,但并发下会撞号——两个人同时点添加,可能拿到同一个 max 值。课程设计不涉及并发,实际小超市单收银台也问题不大,但如果要扩展成多终端,就得换成数据库序列或加锁。这是二次开发时第一个要评估的点。

4. 功能模块实现:登录、商品、职工三条主线的代码拆解

4.1 登录验证模块

报告 6.1 节给了Veri方法,逻辑是拼 SQL 查用户表,SqlDataReader.Read()返回 true 就放行,同时把登录名和 ID 存到静态变量里供后续使用。Java 侧改写时,最大的改动是把字符串拼接换成PreparedStatement,防注入。

public class LoginService { // 返回登录用户ID,失败返回 null public static String verify(String userId, String password) { if (userId == null || userId.trim().isEmpty() || password == null || password.trim().isEmpty()) { return null; } String sql = "SELECT login_id FROM users_info WHERE login_id = ? AND pass_word = ?"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, userId.trim()); ps.setString(2, password.trim()); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { return rs.getString("login_id"); // 登录成功 } } } catch (SQLException e) { e.printStackTrace(); } return null; } }

逻辑说明:先做空值校验,避免拼出WHERE login_id=''这种无效查询;PreparedStatement的占位符由驱动负责转义,用户输入' OR '1'='1也注入不进去。原报告代码用temDR.GetString(0)取第一列,Java 侧用列名login_id更稳,列顺序变了也不受影响。登录成功后,把返回的 ID 存到会话对象里,Swing 里可以用一个Session单例持有当前用户,替代报告里的静态变量MyMeans.Login_ID。

4.2 商品信息管理模块

报告 6.2 节的btnAdd_Click是整份材料里信息量最大的片段:先做四个字段的非空校验,再查重名,然后生成两个编号,执行两条 insert,最后往 ListView 里追加一行。这个流程把"校验—查重—写库—刷界面"四步走得很完整,值得逐段对照。

Java 侧的商品添加服务:

public class GoodsService { // 添加商品,成功返回 true public static boolean addGoods(String name, String typeId, String price, String keep, String remark) throws SQLException { try (Connection conn = DBUtil.getConnection()) { conn.setAutoCommit(false); // 开启事务 try { // 1. 查重 try (PreparedStatement ps = conn.prepareStatement( "SELECT 1 FROM goods_info WHERE full_name = ?")) { ps.setString(1, name); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { conn.rollback(); return false; // 商品名已存在 } } } // 2. 生成商品编号和库存编号 String goodsId = nextId(conn, "goods_info", "merch_id", "G"); // 3. 插入商品 try (PreparedStatement ps = conn.prepareStatement( "INSERT INTO goods_info(merch_id, proffer_id, full_name, kind, unit_cost, others) " + "VALUES (?, ?, ?, ?, ?, ?)")) { ps.setString(1, goodsId); ps.setString(2, typeId); ps.setString(3, name); ps.setString(4, keep); ps.setBigDecimal(5, new BigDecimal(price)); ps.setString(6, remark); ps.executeUpdate(); } // 4. 同步插入库存记录,初始为 0 try (PreparedStatement ps = conn.prepareStatement( "INSERT INTO stock(goods_id, amount) VALUES (?, 0)")) { ps.setString(1, goodsId); ps.executeUpdate(); } conn.commit(); return true; } catch (SQLException e) { conn.rollback(); throw e; } } } }

参数说明:typeId对应报告里的comboxType选中项,实际是供货商编号或商品类别编号,报告代码里取的是My_Set.Tables[0].Rows[comboxType.SelectedIndex][0],即下拉框数据源的第一列。price用BigDecimal而不是double,金额计算不会出现0.1+0.2=0.30000000000000004这种精度问题。keep是保质期,报告里当字符串存,实际可以改成日期字段。

和原代码的差别在于事务:报告代码两条 insert 之间没有begin transaction,第一条成功第二条失败时,商品表会留下一条没有库存记录的孤儿数据。加上setAutoCommit(false)和rollback之后,要么都成功要么都不写,这是二次开发时必须补的一课。

4.3 职工信息管理模块

报告 6.3 节只贴了btnSave_Click的开头,逻辑和商品添加类似:校验姓名非空,生成职工编号,插入staff_bill。职工表的字段里id_card是NOT NULL,但报告代码没校验身份证号,直接插会抛数据库异常。复现时要在界面层补一个 18 位长度校验,或者把该字段改成可空。

职工模块的删除操作有个隐藏依赖:如果该职工在别处被引用(比如销售记录里记了操作人),直接删会破坏数据完整性。报告的表设计里销售表没有职工外键,所以删除是安全的,但二次开发如果加了"销售员"字段,删除前就得先检查引用。常见做法是软删除,加一个status字段标记离职,而不是物理删除。

4.4 进货与销售模块的库存联动

报告 6.5 和 6.6 节分别讲添加销售信息和进货信息,但没贴完整代码。从业务逻辑推断,这两个模块的核心是"写流水 + 改库存"两步。进货时库存增加,销售时库存减少,且销售前要检查库存是否足够。

// 销售:扣减库存,库存不足返回 false public static boolean sell(String goodsId, int amount) throws SQLException { try (Connection conn = DBUtil.getConnection()) { conn.setAutoCommit(false); try { // 1. 查当前库存并加行锁 int stock; try (PreparedStatement ps = conn.prepareStatement( "SELECT amount FROM stock WHERE goods_id = ? FOR UPDATE")) { ps.setString(1, goodsId); try (ResultSet rs = ps.executeQuery()) { if (!rs.next()) { conn.rollback(); return false; } stock = rs.getInt("amount"); } } if (stock < amount) { conn.rollback(); return false; } // 2. 写销售流水 String sellId = nextId(conn, "sell", "sell_id", "S"); try (PreparedStatement ps = conn.prepareStatement( "INSERT INTO sell(sell_id, goods_id, amount, date) VALUES (?, ?, ?, NOW())")) { ps.setString(1, sellId); ps.setString(2, goodsId); ps.setInt(3, amount); ps.executeUpdate(); } // 3. 扣库存 try (PreparedStatement ps = conn.prepareStatement( "UPDATE stock SET amount = amount - ? WHERE goods_id = ?")) { ps.setInt(1, amount); ps.setString(2, goodsId); ps.executeUpdate(); } conn.commit(); return true; } catch (SQLException e) { conn.rollback(); throw e; } } }

FOR UPDATE是这里的关键:查库存时锁住该行,防止两个收银台同时读到 10 件、各卖 8 件、最后库存变成 -6。单机场景用不上,但代码里加上不亏,属于"现在不写以后补起来更痛"的那类细节。进货逻辑对称,把-换成+,流水表换成buy即可。

5. 避坑与常见问题:复现这份报告时最容易翻车的五处

5.1 表结构前后矛盾,直接执行建表脚本报错

现象:照抄报告 5.3 节的CREATE TABLE Stock语句,数据库提示StockID列不存在。原因:报告里字段列表只写了GoodsID和Amount,主键却引用了StockID,是文档编辑时的遗漏。解决:以 5.2.7 节的字段定义表为准,库存表主键用GoodsID,或者补一个自增的StockID并把GoodsID设为唯一索引。我一般选前者,商品和库存一对一,GoodsID做主键最自然。

5.2 中文乱码,商品名存进去变成问号

现象:添加商品后,列表里显示???。原因:数据库字符集、连接串字符集、JDBC 驱动三者不一致。报告用的是 SQL Server,复现换 MySQL 后,建库时没指定utf8mb4,或者连接串漏了characterEncoding=utf8。解决:建库语句带DEFAULT CHARACTER SET utf8mb4,连接串加useUnicode=true&characterEncoding=utf8,MySQL 8 驱动还要确认serverTimezone参数,否则可能连不上。三处对齐后乱码消失。

5.3 登录成功但后续操作提示未登录

现象:登录界面验证通过,进主界面后点任何功能都报"请先登录"。原因:报告代码把登录状态存在DataClass.MyMeans.Login_ID静态变量里,但登录成功后执行了My_con.Close()和Dispose(),如果后续模块重新 new 了一个MyMeans实例,静态变量可能被重置。解决:Java 侧用独立的Session单例存当前用户,和数据库连接对象解耦,连接用完就关,会话状态单独维护。这是典型的"状态和资源混在一起"的坑。

5.4 删除商品时外键约束报错

现象:在商品管理界面点删除,弹出Cannot delete or update a parent row。原因:该商品在buy或sell表里有流水记录,外键挡住了删除。报告的表设计里外键没加ON DELETE CASCADE,这是对的——流水不能跟着商品一起消失。解决:删除前先查流水表,有记录就提示"该商品存在进出货记录,不能删除",或者改成软删除,加is_deleted字段,查询时过滤。直接级联删除是后悔药都买不到的操作。

5.5 编号生成撞号,主键冲突

现象:连续快速添加两条商品,第二条报Duplicate entry 'G00002'。原因:nextId先查MAX再插入,两步之间没有锁,并发时两个请求拿到同一个最大值。解决:单机场景可以在nextId方法上加synchronized,或者把整个添加流程包在一个事务里并对编号表加行锁。更彻底的做法是用数据库自增列或序列表,但报告用的是定长字符编号,改成自增要动界面显示逻辑,权衡后加锁更省事。

6. 二次开发改造思路:从课程设计到能用的原型

拿到这套代码,最值钱的改造不是换界面皮肤,而是把三处"课程设计妥协"补成生产可用的样子。

第一处是事务边界。报告里商品添加的两条 insert、销售的三步操作,都没有显式事务。改造时统一在 Service 层开setAutoCommit(false),所有写操作走同一个Connection,成功commit、异常rollback。验证方法很简单:在第二条 insert 前手动抛一个异常,看商品表是否留下脏数据,改造前会留,改造后不会。

第二处是输入校验下沉。报告把校验全写在按钮点击事件里,if (txtName.Text == "")一路 else if 下去,字段一多就成面条。改造时抽一个Validator工具类,用注解或配置描述每个字段的规则(非空、长度、数字范围),界面层只调Validator.validate(form)返回错误列表。这样加字段不用改事件代码,也方便后面接 Web 端复用同一套规则。

第三处是报表与统计。报告只做到增删改查,没有经营分析。库存视图v_stock_sas已经是个好的起点,往上加两个查询就能出日报:按日期聚合sell表算日销售额,按商品聚合算销量排行。下面这个查询可以直接挂到主界面的"今日概览"面板上。

-- 今日销售汇总:总单数、总件数、总金额 SELECT COUNT(*) AS order_count, SUM(amount) AS total_items, SUM(amount * g.unit_cost) AS total_amount FROM sell s JOIN goods_info g ON s.goods_id = g.merch_id WHERE DATE(s.date) = CURDATE(); -- 近 7 天销量前 5 的商品 SELECT g.full_name, SUM(s.amount) AS sold FROM sell s JOIN goods_info g ON s.goods_id = g.merch_id WHERE s.date >= DATE_SUB(CURDATE(), INTERVAL 7 DAY) GROUP BY g.full_name ORDER BY sold DESC LIMIT 5;

注意SUM(amount * g.unit_cost)用的是商品当前单价,不是销售时的成交价。如果要做准确的毛利分析,sell表得加一个unit_price字段,销售时把当时单价快照进去。这是我在类似项目里踩过的坑:一开始图省事用当前价算历史销售额,商品调价后历史报表全变了,对不上账。从那以后我每次做流水表,都把成交单价、成交数量、金额三个字段一起落库,宁可多存一列,也不让报表依赖可变数据。

改造完这三处,这套系统就从"能演示"变成"能给小超市用"。再往上走,可以加多用户权限(报告 6.8 节提了权限设置但没展开)、加条码扫描枪输入(Swing 里监听键盘事件即可)、把数据库换成 SQLite 做单文件部署。每一步都不大,但每一步都得先把事务和校验的地基打牢,否则功能越多,数据越乱。希望这份拆解能帮你少走几段弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询