Java课设实战:小型自选商场商品管理系统数据库与JDBC全解析

发布时间:2026/10/10 9:34:47
Java课设实战:小型自选商场商品管理系统数据库与JDBC全解析
简介这是一份基于C#与SQL Server的小型自选商场商品管理系统完整项目面向需要完成数据库课程设计或进销存系统开发的学习者。系统覆盖进货、售货、库存动态刷新、供应商查询及收银台等环节并设计了库存、售货、进货、供应商四张核心数据表基本对应小型超市日常管理需求。资源包含189个文件压缩包约680KB核心为60个SQL脚本与54个C#源文件另有窗体界面资源、配置文件及可执行程序可直接还原数据库并运行查看效果。项目采用分层目录结构便于理解数据访问与界面交互逻辑。目前已有1025人学习下载适合初步掌握C#和SQL基础、希望参考完整进销存业务流程的读者。通过源码可快速掌握多表关联、事务更新库存、按月统计及收银小票生成等关键实现思路。1. 小型自选商场商品管理系统一个课设级数据库项目能学到什么做课程设计最怕听到的题目就是“做个商品管理系统”。听着简单真下手才发现一个给小型自选商场用的商品管理系统背后串着数据库建模、库存流水、报表统计一整条链路。这套资源就是围绕“数据库商品管理系统”这个核心搭的从建库SQL、JDBC数据访问到Swing界面把商品入库、收银、盘点、模糊查询完整走通。适合有Java基础但还没独立做完过增删改查系统的人也适合期末课设急着要一个能讲清楚、能现场演示完整项目的人。至于它值不值得下看完后面几章的坑你会自己判断。2. 先建模再写代码表结构设计与七个模块的取舍2.1 功能模块与角色边界管理员、收银员、库存操作员各管一摊这个项目把系统切成三个角色对应三类界面。管理员管用户、看报表收银员只管开单收款库存操作员只处理出入库。角色拆开之后数据库权限也容易分收银员只读商品表、写销售表库存操作员只写出入库表。我一般会在login之后拿到role字段界面根据role值动态渲染菜单而不是给所有用户同一个全功能界面。这样答辩时能讲出“权限设计”这个点而不是只能说“我做了个登录”。界面侧还有一个细节不同角色看到的首页不一样。管理员进门先看到今日销售额和低库存提醒收银员进门直接是收银台。这个设计不用复杂的框架登录后switch一下角色字符串setVisible(true)对应的JPanel就行。但别小看这一点很多课设项目就是把所有按钮堆在一个窗口里答辩被问“权限怎么做的”就卡壳。2.2 七张核心表从商品表到销售流水表几个字段设计要记牢表设计是这个项目的灵魂。整理下来一共七张表用户表、商品信息表、商品类别表、供应商表、入库记录表、销售流水表、库存表。设计上有几个关键取舍都是踩过坑得出的结论。金额字段必须用DECIMAL(10,2)不能用float/double。浮点累计会出现0.10.2不等于0.3的问题账面差一分钱最后对不上账这是高频翻车点。商品状态用TINYINT1上架0下架查询时一律加status 1条件下架商品不在收银台出现。销售流水表里要冗余存一份“商品名称”和“销售时单价”。如果只存商品ID回头改价或改名称历史流水全跟着变报表算出来就失真。冗余字段换来的是历史快照这个词答辩时说出来会显得你考虑过数据一致性。库存表单独一张入库和销售都去更新它不把库存字段直接放到商品表里。分开的好处是盘点时直接对库存表做UPDATE不用碰商品主表并发度也更高。供应商表放不放?放。虽然小商场可能就三五个供应商但有这张表入库单上就能带供应商ID后面做“按供应商统计进货额”的报表就是一条JOIN的事。没有这张表入库记录就是孤立数据报表维度少一个。2.3 建库脚本与字符集建表SQL里的参数直接决定后续省不省心资源包里的建库脚本是MySQL 8的写法字符集用utf8mb4。下面是精简后的建表脚本只保留核心三张表作示例完整版在资源包的sql目录里。CREATE DATABASE IF NOT EXISTS shop_mall DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE shop_mall; CREATE TABLE goods ( goods_id INT PRIMARY KEY AUTO_INCREMENT, goods_name VARCHAR(64) NOT NULL, category_id INT NOT NULL, sale_price DECIMAL(10,2) NOT NULL, stock_count INT NOT NULL DEFAULT 0, status TINYINT DEFAULT 1, INDEX idx_category (category_id) ) ENGINEInnoDB COMMENT商品基本信息表; CREATE TABLE sale_record ( record_id INT PRIMARY KEY AUTO_INCREMENT, goods_id INT NOT NULL, goods_name VARCHAR(64) NOT NULL, sale_price DECIMAL(10,2) NOT NULL, quantity INT NOT NULL, total_amount DECIMAL(10,2) NOT NULL, sale_time DATETIME DEFAULT CURRENT_TIMESTAMP, cashier_id INT NOT NULL ) ENGINEInnoDB COMMENT销售流水表; CREATE TABLE stock ( goods_id INT PRIMARY KEY, current_count INT NOT NULL DEFAULT 0, last_update_time DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP ) ENGINEInnoDB COMMENT库存表;几个参数说明AUTO_INCREMENT做主键避免业务ID重复TINYINT做状态位够用不需用INT浪费空间DECIMAL(10,2)的10表示整数部分最多10位、2位小数对这种小商场完全够。stock_count在goods表里也放了一份是为了列表查询不用去JOIN库存表这个冗余和销售流水冗余一个道理——读多写少就冗余写多读少就别冗余。外键我建议不加靠代码层维护一致性加外键后期导数据、删数据都麻烦MySQL外键还有额外的锁开销。3. JDBC连接与DAO层搭一个能跑不翻车的骨架3.1 连接管理单例Connection与四个关键连接参数资源包里用的是JDBC MySQL没有引重量级框架。连接管理是第一个坑。裸写DriverManager.getConnection每次操作都开连接本地测试没问题跑一会儿就抛Too many connections。常见做法是用单例封装一个连接持有配合短任务够用如果要上连接池就换HikariCP但课设阶段单例足够。下面是资源里的DBUtil四个参数每一个都是经验值。public class DBUtil { private static final String URL jdbc:mysql://localhost:3306/shop_mall ?useSSLfalsecharacterEncodingutf8serverTimezoneAsia/ShanghaiallowPublicKeyRetrievaltrue; private static final String USER root; private static final String PASSWORD 123456; private static Connection conn; public static Connection getConnection() throws SQLException { if (conn null || conn.isClosed()) { try { Class.forName(com.mysql.cj.jdbc.Driver); } catch (ClassNotFoundException e) { throw new SQLException(驱动加载失败检查mysql-connector-j.jar是否在classpath, e); } conn DriverManager.getConnection(URL, USER, PASSWORD); } return conn; } }这四个参数逐一说一下。useSSLfalse本地开发不开加密省掉TLS握手时间MySQL 8默认是关闭SSL的你硬开反而要配证书。characterEncodingutf8不写这个中文写入就是乱码后面排错会浪费两小时。serverTimezoneAsia/ShanghaiMySQL 8不指定时区驱动会拿UTC查出来时间差8小时做日结报表时数据全错。allowPublicKeyRetrievaltrueMySQL 8默认caching_sha2_password认证不开这个首次连接会报Public Key Retrieval is not allowed。这四样写进URL能避开80%的启动即报错。连接持有这里有个细节isClosed()方法并不真的向数据库发消息只是看本地socket有没有关。如果MySQL重启了本地socket还活着isClosed()返回false拿着旧连接去执行SQL才会爆出Communications link failure。所以更稳的写法是加conn.isValid(3)验证这个方法是JDBC 4开始自带的会真的向数据库发一个ping超时3秒无效就重连。3.2 DAO层的读写分离PreparedStatement与参数绑定代码里DAO层的核心原则查询方法返回List或单个实体写入方法返回受影响行数全部用PreparedStatement。预编译的价值有两个防止SQL注入以及MySQL执行计划复用。写SQL时拼字符串是绝对禁止的尤其商品名称这种用户输入字段一笔拼接就是一次注入漏洞。下面是商品关键词查询的DAO方法。public ListGoods searchByKeyword(String keyword) throws SQLException { String sql SELECT goods_id, goods_name, sale_price, stock_count FROM goods WHERE goods_name LIKE ? AND status 1; ListGoods result new ArrayList(); try (Connection c DBUtil.getConnection(); PreparedStatement ps c.prepareStatement(sql)) { ps.setString(1, % keyword %); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { Goods g new Goods(); g.setGoodsId(rs.getInt(goods_id)); g.setGoodsName(rs.getString(goods_name)); g.setSalePrice(rs.getBigDecimal(sale_price)); g.setStockCount(rs.getInt(stock_count)); result.add(g); } } } return result; }逻辑说明SQL里只有一个?占位符值在setString(1, % keyword %)这步拼好让MySQL当成字符串参数而不是SQL语法的一部分。特别注意LIKE模糊匹配的百分号要放在参数里而不是写LIKE %?%那样驱动会认为?是字面量查出来永远是空。try-with-resources保证ResultSet、PreparedStatement、Connection三层都自动关闭避免连接泄漏。查出的结果用getBigDecimal取价格不用getDouble。3.3 Swing界面与业务解耦事件里只调service不写SQL界面层用的是JFrame JTable。最常见的翻车代码是把SQL直接写在按钮的ActionListener里。这个资源的管理方式是界面→Service→DAO三层。按钮里只写service调用SQL只活在DAO里。这样改字段名、加权限、换数据库都不用碰界面代码。三层结构在课设答辩里几乎是必问项你得能画出这个调用链。JTable的刷新有个坑改了数据后直接调table.repaint()是无效的因为TableModel没变Swing根本不知道数据变了。正确做法是重新给JTable setModel代码里封装了一个refreshTable()方法从service查到最新的List再构建DefaultTableModelsetDataVector赋值回去。另一个规范是界面线程Swing是单线程模型长查询如果直接在事件回调里跑窗口会白屏卡死。资源里查询用了SwingWorker的doInBackground回调里再更新表格界面不会冻结。如果不想写SwingWorker至少用EventQueue.invokeLater把更新动作包进去。4. 核心业务落地入库、收银、查询的事务与参数细节4.1 入库流程先写流水再更新库存一个事务包到底入库不是一个INSERT就结束的事。它同时要动两张表入库记录表记录进了什么、进价多少、进了多少库存表把数量累加。中间任何一步失败两边就对不上账。所以入库必须包在一个事务里。public boolean stockIn(StockInRecord record) throws SQLException { Connection conn DBUtil.getConnection(); int affected 0; try { conn.setAutoCommit(false); // 1. 写入库流水 String insertSql INSERT INTO stock_in(goods_id, quantity, cost_price, in_time, operator_id) VALUES (?, ?, ?, NOW(), ?); try (PreparedStatement ps conn.prepareStatement(insertSql)) { ps.setInt(1, record.getGoodsId()); ps.setInt(2, record.getQuantity()); ps.setBigDecimal(3, record.getCostPrice()); ps.setInt(4, record.getOperatorId()); affected ps.executeUpdate(); } // 2. 更新库存表 String updateSql UPDATE stock SET current_count current_count ? WHERE goods_id ?; try (PreparedStatement ps conn.prepareStatement(updateSql)) { ps.setInt(1, record.getQuantity()); ps.setInt(2, record.getGoodsId()); affected ps.executeUpdate(); } conn.commit(); return affected 2; } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); } }逻辑说明先setAutoCommit(false)关掉自动提交两条SQL都成功才commit任何异常直接rollbackfinally里把autoCommit还原成true避免后续连接复用出问题。affected判断返回2表示两张表都动了。这里有个边界情况库存表里没有对应goods_id怎么办?常见做法是建库时就把商品表里所有ID初始化进stock表INSERT INTO stock(goods_id) SELECT goods_id FROM goods一次搞定。如果没初始化UPDATE影响行数是0入库就失败排查半天找不到原因。4.2 收银扣库存UPDATE带库存条件一次性避免超卖收银场景是并发冲突的高发区。两个收银员同时卖同一件只剩1件的商品如果代码是先SELECT库存、数量够再UPDATE两次SELECT都看到1两个UPDATE都成功库存就变成负数。正确做法是把判断条件写进UPDATE语句一次原子操作完成扣减。public boolean deductStock(int goodsId, int quantity) throws SQLException { Connection conn DBUtil.getConnection(); try { conn.setAutoCommit(false); String deductSql UPDATE stock SET current_count current_count - ? WHERE goods_id ? AND current_count ?; PreparedStatement ps conn.prepareStatement(deductSql); ps.setInt(1, quantity); ps.setInt(2, goodsId); ps.setInt(3, quantity); int rows ps.executeUpdate(); if (rows 0) { conn.rollback(); return false; // 库存不足订单作废 } // 写销售流水省略具体SQL conn.commit(); return true; } catch (SQLException e) { conn.rollback(); throw e; } finally { conn.setAutoCommit(true); } }核心在WHERE current_count ?。MySQL的UPDATE在InnoDB下会对匹配行加行锁两个事务同时执行时第二个会等到第一个commit然后发现current_count已经不满足条件受影响行数为0返回false收银端提示库存不足。这样就从机制上杜绝了超卖不需要select for update不需要额外锁表。注意受影响行数判断的是物理更新MySQL的默认行为是只更新值变化了的才算行数;如果quantity传入0current_count没有实际变化也会返回0所以调用方要先做参数校验quantity必须大于0。4.3 模糊查询与分页LIKE占位符和LIMIT参数的安全写法商品查询是使用频率最高的功能列表页不做分页数据库里数据一多界面卡成黑匣子。分页SQL的标准写法是LIMIT offset, pageSize两个参数都走占位符绑定。public ListGoods queryByPage(String keyword, int pageNo, int pageSize) throws SQLException { String sql SELECT goods_id, goods_name, sale_price, stock_count FROM goods WHERE status 1 AND goods_name LIKE ? ORDER BY goods_id LIMIT ?, ?; ListGoods list new ArrayList(); try (Connection conn DBUtil.getConnection(); PreparedStatement ps conn.prepareStatement(sql)) { ps.setString(1, % keyword %); ps.setInt(2, (pageNo - 1) * pageSize); ps.setInt(3, pageSize); try (ResultSet rs ps.executeQuery()) { while (rs.next()) { // 组装Goods对象 } } } return list; }LIMIT的两个参数都是整数占位符不能拼接否则有注入风险。pageNo从1开始计算offset要减1这是新手最容易写错的地方。ORDER BY字段在示例中是goods_id主键排序稳定且性能好。如果你改成ORDER BY sale_time就要注意时间相同的两条记录在翻页时可能重复或丢失——因为排序不唯一MySQL不保证稳定的分页顺序。数据量大时我一般不走OFFSET改用上一页最后一条记录的ID做游标但课设场景OFFSET够用。5. 避坑与常见问题排查五条血泪踩坑记录5.1 界面显示“口口口”控制台中文也乱码现象JFrame标题、按钮文字全部变成方框MySQL里查出来也是“????”。原因两层问题叠在一起。第一层是数据库和连接没统一字符集建库用了latin1或者连接串没带characterEncodingutf8。第二层是Swing默认字体不含中文字形Windows下用默认字体渲染中文会缺字形。缺字形和乱码是不同问题但现象很像容易被一起归因。解决建库必须显式指定utf8mb4连接串统一加characterEncodingutf8。Swing层面把组件字体设为“微软雅黑”或“宋体”根面板设置一次子组件大部分会继承。从那以后我每次建新项目第一件事就是确认全套字符集项目编码、数据库、连接串一个不落第二步才谈写代码。5.2 程序放一晚上第二天操作报连接超时现象头天晚上程序没关第二天点查询抛SQLException: Communications link failure重启程序又好了。原因MySQL的wait_timeout默认8小时空闲连接超过8小时被服务端主动断开。程序里的单例Connection还握着旧socket一用就发现连接已死报连接失败。解决两个方案二选一。一是在URL上配autoReconnecttrue但MySQL 8官方文档已经说这个参数不建议依赖不保证每次生效;二是在DBUtil的getConnection里加连接验证每次拿连接时调用isValid(3)无效就重建。isValid是JDBC 4自带的方法会真正向服务端发ping3秒超时是省事的自愈方案。我一般用第二个不依赖被废弃的参数。5.3 库存被扣成负数账面上商品数对不上现象两台收银机同时卖同一件只剩1件的商品最后库存变成-3销售流水却卖出去十几件。原因代码用了“SELECT库存→判断够不够→UPDATE”三步走两个事务在SELECT时都看到同样的库存值判断都通过然后各自UPDATE库存就被减穿了。这是典型的读改写竞态。解决把判断条件写进UPDATE的WHERE里改成单条原子语句。UPDATE stock SET current_count current_count - ? WHERE goods_id ? AND current_count ?受影响行数为0就说明卖超了事务回滚不写销售流水。这个改法不需要锁表不需要select for update性能影响最小。从那以后凡是涉及数量增减的SQL我第一反应都是找出那条UPDATE把条件写进去。5.4 销售报表SUM翻倍对账对不上现象按商品汇总销量导出来的报表总金额是实际流水的两倍多怎么都对不上。原因汇总SQL把销售流水表和商品表直接JOIN。如果再把类别表也JOIN进来JOIN的结果集就会产生笛卡尔乘积某些行被重复匹配SUM就翻倍。同一个类别下5个商品、3个类别JOIN错一张表就可能多出5倍行数。解决避免多表JOIN做聚合。先子查询分组得出一张临时表再和其他维度表关联。例如要按类别汇总销售额先对sale_record按goods_id分组SUM再把结果和category表关联取类别名。这个教训我记了三年聚合之前先看一眼FROM后面跟了几张表超过一张就要怀疑重复行。查出来的SUM和明细对不上优先怀疑JOIN膨胀别急着怀疑数据录错。5.5 课设打包成JAR后双击报错驱动类和数据库都连不上现象IDE里F5跑得好好的Export成一个可运行JAR放到别的电脑双击就报ClassNotFoundException或者Communications link failure。原因两件事。ClassNotFoundException是mysql-connector的jar没被打进JAR包很多IDE导出时默认不带依赖需要选导出Runnable JAR并勾选Package required libraries。连接失败则大概率是MySQL 8的认证插件问题目标环境没开allowPublicKeyRetrieval驱动拿不到公钥直接拒绝握手。解决MySQL 8的URL必须带allowPublicKeyRetrievaltrue和useSSLfalse两个参数缺一个都可能在冷启动时报错。打包含依赖选Runnable JAR file导出而不是普通的JAR file。还有第三层坑目标机器上MySQL的root账号可能只允许localhost登录换一台机器自然连不上。解决是在建账号时用root%或者把密码改成目标机器可用的账号。这三个问题叠在一起时报错信息会互相掩盖排查顺序我建议先看驱动有没有打进包再看URL参数最后看账号权限。6. 数据导入导出与报表把“能跑”变成“可用”的收尾6.1 CSV批量导入准备数据比人工录入快十倍很多课设项目交差的标准是“能增删改查就行”但答辩时老师随手打开一个CSV问“1000条商品怎么录进去”答不上来就尴尬了。我这里保留了一个“初始化导入”功能放一个CSV文件里面三列商品名称、类别、售价。用JDBC的PreparedStatement批量执行每100条提交一次事务比单条INSERT快一个数量级。核心就两行String csvLine 可乐,饮料,3.50; String[] parts csvLine.split(,); String sql INSERT INTO goods(goods_name, category_id, sale_price) VALUES (?, ?, ?);注意CSV解析别用split(,)一步到位字段里如果带逗号就崩了。常见做法是先用CSV库解析一行得到一个字段数组再绑定参数。类别那一列进来是中文名得先查一下category表把字符串换成ID这个小转换逻辑建议写个Map缓存别逐条查询。6.2 月度报表与白纸安装验证交付前把坑重新踩一遍报表的通用思路是写SQL做分组统计然后把ResultSet循环写成CSV用Files.write落到本地Excel打开就能看。月度报表按天分组商品畅销榜按数量倒序这两条是最常用的。报表SQL有个高频坑按“天”分组不是GROUP BY sale_time而是GROUP BY DATE(sale_time)因为DATETIME带时分秒直接分组会把同一天的流水拆成好几行日报表数字就对不上。所有代码写完后我会把数据库删了重建一遍按文档里的步骤重新跑一次。这个动作能暴露一半的问题——建库脚本少一条分号、类名打错、路径写死、root密码没改。每次交付前我都强制走一遍白纸安装流程确认从建库到登录全程无障碍最后把资源包里的SQL脚本、源码和说明文档放一起提交。这份资源的完整代码骨架和建库脚本都在包里拿回去对着文档敲一遍比自己从零开始能省一整天的排错时间。希望帮到你。本文还有配套的精品资源点击获取