
简介基于Java Swing和MySQL的仓库管理系统设计源码是一套面向开发者、学生和企业技术人员的完整仓储管理系统实现目标是用系统化方式解决传统人工记账效率低、库存信息不透明的问题。系统覆盖商品入库、出库、库存盘点、报表统计、权限控制与操作日志等功能源码按dao、entity、frame、utils等包划分层次清楚适合二次开发和教学演示。压缩包共39个文件大小8.45MB包含15个Java源文件、10个JAR依赖包、7个XML配置文件以及Git忽略文件、PNG图标、IML项目文件与说明文档各文件类型分工明确。目前已有118人学习可用于课程设计、毕业设计或中小型仓储管理项目参考。读者拿到的是完整可运行的源码既能理解Swing界面构建与MySQL数据交互也能借鉴其事务处理和权限管理思路便于在此基础上扩展新功能。1. 基于Java Swing和MySQL的仓库管理系统这套源码到底在解决什么问题基于Java Swing和MySQL的仓库管理系统在Java课程设计、毕业设计和中小公司内部工具里反复出现。它不新但很耐做一套界面、几张业务表、一连串事务刚好把Java基础的关键知识点串联起来。一家做五金批发的小仓库每天进出几十单货老板只想知道某个螺丝还有多少上Web系统太重Excel又容易改错这正是这套组合的典型栖息地。这套设计源码要交付的不只是能跑的界面而是一套能讲清楚的方案数据库建模怎么才能不产生脏库存Swing窗口怎么组织才不至于开十几个窗口乱飞入库出库怎么保证库存数字不花。适合三类人做课程设计的学生、想练桌面端CRUD的Java新手、需要给内部库房做工具又不想上Web的工程师。按我实际落地的做法从建表、写界面、做业务到打包交付一步步拆开讲。2. 先建模再写界面核心表结构与JDBC连接参数怎么定2.1 为什么表结构比界面更重要从业务流倒推数据模型很多新手拿到仓库管理系统这个题目第一反应是打开Swing拖控件。这个顺序是反的。界面只是壳仓库系统的灵魂在数据模型。先想清楚业务流采购入库带来库存增加销售出库带来库存减少盘点把账面数修正为实盘数。每一笔变动都要留有单据否则三个月后想查这批货什么时候进的、谁经手的根本无从下手——这是仓库管理系统和普通CRUD练习最大的区别。最常见的翻车设计是只建一张goods表里面放一个quantity字段入库就加、出库就减不做单据、不留流水。这种设计demo跑起来很爽但库存一旦错了你根本不知道错在哪一步。所以我一般会按单据头 单据明细 库存余量三层来建模单据记录业务事实库存只是可变的计算结果。这个思路直接决定了后面所有mysql事务处理的写法——库存是结果单据是事实两者必须一起提交或一起回滚。2.2 建库建表SQL字段类型、唯一键和索引的取舍建库用utf8mb4字符集引擎统一InnoDB。MyISAM不是不能用但它不支持事务和行级锁而仓库系统最怕的恰恰就是操作到一半失败库存却加了一半。CREATE DATABASE IF NOT EXISTS warehouse_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE warehouse_system; -- 用户表 CREATE TABLE user ( id INT AUTO_INCREMENT PRIMARY KEY, username VARCHAR(50) NOT NULL UNIQUE COMMENT 登录名, password VARCHAR(64) NOT NULL COMMENT MD5密文, real_name VARCHAR(50) COMMENT 真实姓名, role VARCHAR(20) NOT NULL DEFAULT staff COMMENT admin/staff, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT用户; -- 仓库表 CREATE TABLE warehouse ( id INT AUTO_INCREMENT PRIMARY KEY, warehouse_name VARCHAR(50) NOT NULL, address VARCHAR(200), status TINYINT NOT NULL DEFAULT 1 COMMENT 1启用 0停用 ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT仓库; -- 商品表 CREATE TABLE goods ( id INT AUTO_INCREMENT PRIMARY KEY, goods_code VARCHAR(32) NOT NULL UNIQUE COMMENT 商品编码, goods_name VARCHAR(100) NOT NULL, category_id INT COMMENT 商品分类可关联category表, spec VARCHAR(100) COMMENT 规格型号, unit VARCHAR(20) COMMENT 单位件/箱/千克, safety_stock INT NOT NULL DEFAULT 0 COMMENT 安全库存, status TINYINT NOT NULL DEFAULT 1 COMMENT 1上架 0下架, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_category (category_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT商品; -- 库存表一个商品在一个仓库只有一条记录 CREATE TABLE stock ( id INT AUTO_INCREMENT PRIMARY KEY, goods_id INT NOT NULL, warehouse_id INT NOT NULL, quantity INT NOT NULL DEFAULT 0, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, UNIQUE KEY uk_goods_warehouse (goods_id, warehouse_id), CONSTRAINT fk_stock_goods FOREIGN KEY (goods_id) REFERENCES goods (id), CONSTRAINT fk_stock_warehouse FOREIGN KEY (warehouse_id) REFERENCES warehouse (id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT库存; -- 入库单头 CREATE TABLE stock_in_order ( id INT AUTO_INCREMENT PRIMARY KEY, order_no VARCHAR(32) NOT NULL UNIQUE COMMENT 单号, warehouse_id INT NOT NULL, supplier_name VARCHAR(100) COMMENT 供应商简单场景直接存名字, operator_id INT NOT NULL COMMENT 操作人, remark VARCHAR(255), created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_warehouse_time (warehouse_id, created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT入库单; -- 入库明细 CREATE TABLE stock_in_item ( id INT AUTO_INCREMENT PRIMARY KEY, order_id INT NOT NULL, goods_id INT NOT NULL, quantity INT NOT NULL COMMENT 入库数量, price DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT 单价, KEY idx_order (order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT入库明细;出库的两张表和入库对称结构一样只把order_no前缀改成OUT、语义从入库变成出库这里不重复贴。表字段的取舍有几个点值得说透。第一金额一律用DECIMAL(10,2)不要用DOUBLE或FLOAT。浮点数在二进制里表示不精确0.1加0.2这种账目误差在财务数据里是事故DECIMAL是十进制字符串存储算钱才安心。第二stock表上的联合唯一键(goods_id, warehouse_id)是整套设计的基石它保证同一个商品在同一个仓库里只有一行库存记录后面的upsert才能用。第三(warehouse_id, created_at)是给按时间范围查某仓库流水准备的仓库系统查单基本都是这个维度比单字段索引更贴合查询路径。提示MySQL 5.7 和 8.0 的驱动类名不一样5.7 用 com.mysql.jdbc.Driver8.0 用 com.mysql.cj.jdbc.Driver。项目里如果两个 jar 同时存在会互相干扰。核心表汇总一下方便你建表时对照表名用途关键字段设计要点user登录与权限username, password, role密码存MD5或bcrypt不存明文warehouse仓库主数据warehouse_name, status多仓是库存唯一键的一部分goods商品主数据goods_code, goods_name, spec编码唯一分类可独立成表stock库存余量goods_id, warehouse_id, quantity联合唯一键upsert的基础stock_in_order / stock_in_item入库单据order_no, goods_id, quantity, price一单头对多明细stock_out_order / stock_out_item出库单据与入库对称出库时校验库存inventory_check_order / item盘点单据goods_id, diff_quantity差异调整留痕排序字段多说一句所有流水单据的列表查询默认按created_at倒序SQL里写ORDER BY created_at DESC别让用户翻到最后一页才能看到刚录的单。这种看似不起眼的Mysql排序细节恰恰是演示时最容易被老师或老板点出来的体验问题。2.3 JDBC连接驱动版本、URL参数和数据库配置表建好了接下来是Java这边怎么连MySQL。现在主流是MySQL 8.0驱动包用mysql-connector-j驱动类名是com.mysql.cj.jdbc.Driver。如果你还在用5.7驱动类名是com.mysql.jdbc.Driver——很多ClassNotFoundException就是这么来的驱动版本和类名对不上。Class.forName(com.mysql.cj.jdbc.Driver); String url jdbc:mysql://localhost:3306/warehouse_system ?useSSLfalseserverTimezoneAsia/ShanghaicharacterEncodingutf8; Connection conn DriverManager.getConnection(url, root, 123456);URL上几个参数是血泪经验换来的。useSSLfalse关掉SSL握手本机开发能少一堆警告和偶发超时serverTimezoneAsia/Shanghai解决MySQL 8.0的时区报错不设置的话连接时直接抛The server time zone value异常新手第一次看到会以为驱动坏了characterEncodingutf8配合建库时的utf8mb4保证中文从界面到数据库一路不乱码。一个常见做法是把连接参数抽到db.properties里用一个JdbcUtil集中读取而不是在每个DAO里硬编码。理由很现实课程设计答辩或小团队交付时数据库密码、IP大概率要现场改硬编码意味着重新编译打包抽到配置文件则改一行就能跑。URL参数推荐值解决什么问题useSSLfalse本地/内网免SSL握手少告警serverTimezoneAsia/Shanghai报时区异常、日期偏差characterEncodingutf8中文乱码allowPublicKeyRetrievaltrueMySQL 8的caching_sha2_password认证需要连接池这块纯Swing桌面应用、并发用户十几个以内直接用DriverManager每次开连接就行不必上连接池。真到了几十人同时用的规模桌面端本身就不合适了应该考虑Web化。别在课程设计里为了炫技引入三四个框架老师问起来反而难自圆其说。3. Swing界面组织登录窗口、主框架与面板切换的写法3.1 登录窗口组件布局和回车触发登录Swing的界面代码很多人写得又臭又长问题多半出在没用布局管理器而是setBounds硬拼坐标。硬拼坐标在窗口一拉伸、换台高分屏就乱成一团。我一般用GridLayout或GridBagLayout配合BorderFactory.createEmptyBorder留边距界面在不同分辨率下都能保持整洁。public class LoginFrame extends JFrame { private JTextField usernameField; private JPasswordField passwordField; public LoginFrame() { super(仓库管理系统-登录); setSize(380, 240); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); setResizable(false); JPanel panel new JPanel(new GridLayout(3, 2, 8, 8)); panel.setBorder(BorderFactory.createEmptyBorder(20, 20, 20, 20)); panel.add(new JLabel(用户名:)); usernameField new JTextField(16); panel.add(usernameField); panel.add(new JLabel(密码:)); passwordField new JPasswordField(16); panel.add(passwordField); JButton loginBtn new JButton(登录); JButton exitBtn new JButton(退出); panel.add(loginBtn); panel.add(exitBtn); add(panel); loginBtn.addActionListener(e - doLogin()); passwordField.addActionListener(e - doLogin()); exitBtn.addActionListener(e - System.exit(0)); } }几个细节说清楚。setLocationRelativeTo(null)把窗口居中演示时不会出现在屏幕角落显得业余。JPasswordField要单独绑定回车事件因为用户习惯输完密码直接按回车只绑登录按钮的话每次还得去点鼠标。密码用getPassword()拿char[]而不是getText()拿String这是Swing的官方建议——String在常量池里不可控char[]用完可以清掉。课程设计里没人在意这个但面试官可能会问。3.2 登录校验密码加盐哈希而不是明文入库登录逻辑的核心是查不到就留在登录页查到才放行。普通做法是用户名加密码一起查返回null就弹错误提示。private void doLogin() { String username usernameField.getText().trim(); String password new String(passwordField.getPassword()); if (username.isEmpty() || password.isEmpty()) { JOptionPane.showMessageDialog(this, 用户名和密码不能为空); return; } User user userDao.findByUsernameAndPassword(username, Md5Util.encode(password)); if (user null) { JOptionPane.showMessageDialog(this, 用户名或密码错误); passwordField.setText(); return; } dispose(); new MainFrame(user).setVisible(true); }关于密码存储简单项目用MD5就够了但至少要做加盐在原始密码后面拼一段固定盐值再哈希避免两个同密码用户产生相同的密文。Maven里加个commons-codec或者手写一个MessageDigest工具类都行。如果这是要长期上线的系统建议直接上bcrypt代价只是多一个依赖安全性完全不在一个量级。这个点在答辩时提出来很加分属于我知道有风险且知道怎么补。3.3 主框架JMenuBar导航加CardLayout容器切换很多人的课程设计每个功能开一个JFrame点入库登记弹一个窗口点库存查询又弹一个几分钟屏幕上就叠了五六个窗口。这种风格有两个错误一是AltTab切换混乱二是关闭主窗口后子窗口还在进程退不干净。正确的组织方式是一个主JFrame中间区域放一个CardLayout容器用菜单切换不同的业务面板。public class MainFrame extends JFrame { private CardLayout cardLayout; private JPanel contentPanel; public MainFrame(User user) { super(仓库管理系统 - user.getRealName()); setSize(1000, 640); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); cardLayout new CardLayout(); contentPanel new JPanel(cardLayout); add(createMenuBar(user), BorderLayout.NORTH); add(contentPanel, BorderLayout.CENTER); add(createStatusBar(user), BorderLayout.SOUTH); contentPanel.add(new StockInPanel(), stockIn); contentPanel.add(new StockOutPanel(), stockOut); contentPanel.add(new StockQueryPanel(), stockQuery); contentPanel.add(new InventoryPanel(), inventory); } }菜单栏的写法是把JMenuBar、JMenu、JMenuItem逐层组装每个JMenuItem绑定actionCommand或直接lambda在统一的事件方法里调cardLayout.show(contentPanel, name)。面板常驻、状态不丢是这种结构最大的好处用户在入库界面填了一半切去查库存切回来内容还在体验比反复重建窗口好得多。角色权限在这里就能做了。admin登录后菜单栏多一项用户管理staff看不到。实现很简单登录时把User对象传进MainFrame构造函数createMenuBar里根据user.getRole()决定是否add子菜单。多角色系统再往上走才是行级权限的活桌面端一般不需要知道边界就行。商品分类做成swing带勾选框的tree那种勾选树属于锦上添花核心流程跑通之前不必碰。3.4 查询面板JTable配合DefaultTableModel的刷新套路库存查询是所有仓库系统使用频率最高的面板JTable的刷新方式直接影响体验。public class StockQueryPanel extends JPanel { private JTable table; private DefaultTableModel model; public StockQueryPanel() { String[] columns {商品编码, 商品名称, 规格, 仓库, 库存数量}; model new DefaultTableModel(columns, 0) { Override public boolean isCellEditable(int row, int column) { return false; } }; table new JTable(model); table.setAutoCreateRowSorter(true); // 点击表头排序 add(new JScrollPane(table), BorderLayout.CENTER); } public void setRows(ListStockView rows) { model.setRowCount(0); // 清空旧数据 for (StockView row : rows) { model.addRow(new Object[]{ row.getGoodsCode(), row.getGoodsName(), row.getSpec(), row.getWarehouseName(), row.getQuantity() }); } } }刷新表格的标准套路是setRowCount(0)加addRow循环而不是每次都new一个DefaultTableModel再setModel。后者的副作用是会把表头的排序状态、列宽设置全部重置用户刚调好的窗口布局一下就打回原形。isCellEditable必须重写成返回false否则用户双击表格里的数字就能改显示内容——虽然不影响数据库但演示时被点到很尴尬。setAutoCreateRowSorter(true)一行代码让表头支持点击排序这是把mysql排序需求搬到界面上最便宜的方式。4. 入库出库与盘点用事务和行锁保证库存不花4.1 入库登记单据头、明细、库存三步同一事务入库业务拆开是三件事写一张入库单头、写若干条明细、给对应商品的库存加数量。三件事要么全成功要么全失败。只写了单据忘了加库存账面和实物对不上加了库存单据却丢了连对账的依据都没有。所以这三步必须包在同一个事务里。public class StockService { public void stockIn(StockInOrder order, ListStockInItem items) throws Exception { Connection conn null; try { conn JdbcUtil.getConnection(); conn.setAutoCommit(false); // 关闭自动提交事务开始 int orderId insertOrder(conn, order); // 1. 入库单头 for (StockInItem item : items) { item.setOrderId(orderId); insertItem(conn, item); // 2. 入库明细 } for (StockInItem item : items) { mergeStock(conn, order.getWarehouseId(), item.getGoodsId(), item.getQuantity()); } // 3. 库存累加 conn.commit(); // 全部成功才提交 } catch (Exception e) { if (conn ! null) conn.rollback(); // 任一步失败全部回滚 throw new BusinessException(入库失败已回滚 e.getMessage()); } finally { if (conn ! null) conn.close(); } } }事务的开关是setAutoCommit(false)。很多人忘记这一行导致每个SQL执行完立刻提交后面rollback根本没用——这是数据库里多了半截单据最常见的根源。commit放在最后前面任何一步抛异常rollback()把已执行的写操作全部撤销。库存累加用一个INSERT...ON DUPLICATE KEY UPDATE一次SQL完成没有就插入、有就累加INSERT INTO stock (goods_id, warehouse_id, quantity) VALUES (?, ?, ?) ON DUPLICATE KEY UPDATE quantity quantity VALUES(quantity);这条语句依赖第二章里那个(goods_id, warehouse_id)联合唯一键。第一次入库某商品时stock表里没有对应行走INSERT以后再入库唯一键冲突走UPDATE把数量累加。比起先SELECT再判断INSERT还是UPDATE这条SQL是单次往返天然原子不用额外加锁。4.2 出库登记先锁行再判断库存不能是负数出库和入库最大的不同是要先校验库存够不够。校验和扣减是两件事如果不做并发控制两个收银员同时卖最后一个商品两个事务都读到库存还有1都判断够都执行扣减最后库存变成-1。这就是经典的超卖也是Java面试题里数据库并发必问的点。解决办法是让校验加扣减变成原子操作用SELECT...FOR UPDATE把库存行锁住public void stockOut(StockOutOrder order, ListStockOutItem items) throws Exception { Connection conn null; try { conn JdbcUtil.getConnection(); conn.setAutoCommit(false); int orderId insertOutOrder(conn, order); for (StockOutItem item : items) { insertOutItem(conn, item); Integer qty queryStockForUpdate(conn, order.getWarehouseId(), item.getGoodsId()); if (qty null || qty item.getQuantity()) { throw new BusinessException(库存不足商品ID item.getGoodsId() 当前 qty 需要 item.getQuantity()); } decreaseStock(conn, order.getWarehouseId(), item.getGoodsId(), item.getQuantity()); } conn.commit(); } catch (Exception e) { if (conn ! null) conn.rollback(); throw e; } finally { if (conn ! null) conn.close(); } } private Integer queryStockForUpdate(Connection conn, int warehouseId, int goodsId) throws SQLException { String sql SELECT quantity FROM stock WHERE goods_id ? AND warehouse_id ? FOR UPDATE; try (PreparedStatement ps conn.prepareStatement(sql)) { ps.setInt(1, goodsId); ps.setInt(2, warehouseId); try (ResultSet rs ps.executeQuery()) { return rs.next() ? rs.getInt(1) : null; } } }FOR UPDATE的含义是这条SELECT命中并锁住库存行直到当前事务commit或rollback才释放。第二个事务如果也想对同一个商品出库会被阻塞在SELECT处等第一个事务结束才能读到最新库存。这样两个并发出库变成串行执行第二个事务会发现库存不够抛出库存不足。单用户课程设计里永远测不出这个bug但演示时现场多开两个客户端同时下单有FOR UPDATE和没有是两种完全不同的结果。注意FOR UPDATE 必须配合事务使用setAutoCommit(false) 之后才能锁住行直到 commit/rollback。开着自动提交去执行 FOR UPDATE锁会立刻释放等于白写。还有一个容易被忽视的点出库明细的插入要放在扣库存之前并和库存校验交错进行。这样如果出库单有5条明细、第3条库存不够前两条已经扣的库存会随rollback一起撤销不会出现单据没录全但库存少了一块的中间态。4.3 盘点业务差异调整单留痕不直接改库存盘点是最容易做歪的功能。很多初稿把盘点做成一个直接编辑库存数字的窗口管理员手一抖改成多少就是多少没有任何记录。这个设计在业务上不能接受盘点结果需要复核差异需要追溯是谁盘出来的、依据是什么。我一般这样设计盘点流程盘点员选择仓库和盘点范围系统生成一张盘点单把当前账面库存快照进盘点明细盘点员在界面上逐条填写实盘数量系统自动计算差异实盘减账面diff_quantity可能是正数盘盈、负数盘亏确认盘点单后用和入库出库相同的mergeStock逻辑按diff_quantity逐条调整库存盘点单状态从盘点中改成已确认不可再改。CREATE TABLE inventory_check_item ( id INT AUTO_INCREMENT PRIMARY KEY, check_order_id INT NOT NULL, goods_id INT NOT NULL, account_quantity INT NOT NULL COMMENT 账面数, actual_quantity INT NOT NULL COMMENT 实盘数, diff_quantity INT NOT NULL COMMENT 差异实盘-账面, remark VARCHAR(200), KEY idx_check_order (check_order_id) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COMMENT盘点明细;确认盘点时diff_quantity为正就按入库处理为负就按出库处理本质上是复用同一个mergeStock方法。这样做的好处是库存变动永远有单据来源要么来自入库单要么来自出库单要么来自盘点单。导数据、对账、答辩讲库存可追溯时这一条就是最有力的证据。一个实际经验盘点界面上账面数量列要设为不可编辑实盘数量列才可编辑。这是在界面层防呆防止盘点员把账面数也改了导致差异计算失去意义。类似的防呆还有实盘数量输入负数、填字母时直接拦截——数据库层虽然可以加CHECK约束但Swing里用InputVerifier或DocumentFilter提前挡住用户体验好得多。5. SwingMySQL联调避坑5个现场、原因和处理方法Swing加MySQL联调坑的密度比想象中高。下面这5个现场我没少遇到也帮人排过原因基本都是环境、并发和生命周期的问题排查清楚了没一个真玄学。5.1 报错一ClassNotFoundException驱动包看似在却找不到现象程序一启动就抛ClassNotFoundException: com.mysql.cj.jdbc.Driver。原因通常有两个。一是mysql-connector的jar根本没进classpathIDE里导了但没勾选Add to Build Path或者用javac手动编译时没带-cp参数。二是驱动类名写错MySQL 8.0的驱动类是com.mysql.cj.jdbc.Driver5.x的才是com.mysql.jdbc.Driver版本和类名必须对应。解决确认mysql-connector-j-8.x.jar在项目的lib目录并加入Build Path或者用Maven在pom.xml里声明依赖让Maven替你管classpath。dependency groupIdcom.mysql/groupId artifactIdmysql-connector-j/artifactId version8.0.33/version /dependency5.2 报错二点击查询后窗口转圈像死机一样现象界面点查询后整个窗口无响应过几秒甚至几十秒才恢复小数据集也卡。原因把耗时的JDBC查询直接写在了Swing的事件回调里。Swing的界面刷新跑在事件分发线程EDT上DB查询占用着这个线程界面重绘、按钮点击全部排队看起来就是卡死。数据量一大这个假死会变成真死。解决查询放到SwingWorker的doInBackground里拿到结果后在done里更新JTable。SwingWorker是Swing官方给的异步方案比手开Thread更安全因为它保证结果回传还是回到EDT上更新界面。SwingWorkerListGoods, Void worker new SwingWorkerListGoods, Void() { Override protected ListGoods doInBackground() throws Exception { return goodsDao.searchByKeyword(keyword); // 耗时查询在后台 } Override protected void done() { try { ListGoods result get(); tableModel.setRows(result); // 结果回到EDT刷新表格 } catch (Exception e) { JOptionPane.showMessageDialog(MainFrame.this, 查询失败 e.getMessage()); } } }; worker.execute();5.3 报错三中文乱码界面、数据库两边不一致现象界面上输入中文保存后数据库里是问号或者变成????反过来数据库里的中文在JTable里显示成乱码。原因三个环节的字符集不一致。Java源文件不是UTF-8编码JDBC URL没带characterEncodingutf8或者建库建表时用了latin1默认字符集。只要坏一环中文就花。解决三处统一。IDE把项目编码设为UTF-8MySQL建库时用utf8mb4JDBC URL加上characterEncodingutf8。改完之后重启程序再插入数据验证一遍。还有一个容易被忽略的坑MySQL连接参数如果写成characterEncodingutf8mb4驱动会报Unsupported character encoding注意这里要写utf8不是utf8mb4。5.4 报错四库存被扣成负数并发下超卖现象两个人同时给同一个商品出库库存明明只剩1两张单都成功了库存变成-1。原因先查库存、判断够不够、再更新库存三步不是原子的。两个事务都读到库存1都认为可以出库然后各自扣减。没有把读和写锁在一起校验就形同虚设。解决用第四章的SELECT...FOR UPDATE把库存行锁住配合事务让校验和扣减串行。如果不想写原生SQL锁可以把库存更新改成带条件的UPDATEUPDATE stock SET quantity quantity - ? WHERE goods_id? AND warehouse_id? AND quantity ?受影响行数为0就说明库存不足抛异常。这个写法也能防超卖而且少一次SELECT。5.5 报错五IDE里能跑双击JAR就报数据库连不上现象Maven打包后拿到另一台机器双击运行立刻报Connection refused或ClassNotFoundException。原因三件事没做好。数据库驱动jar没有打进最终的JAR用的是默认的瘦JAR数据库地址写死在代码里换机器就没法改目标机器上MySQL服务没启动或者账号权限不对。解决用maven-assembly-plugin或shade插件打胖JAR把mysql-connector等依赖一并打包数据库连接参数外置到db.properties运行目录放一份配置文件改IP、密码不用重新编译交付前在一台干净机器上做一次完整验证包括MySQL安装、导入SQL脚本、双击JAR运行三步。这条流程自己先跑一遍交付时才不会在客户面前翻车。6. 交付前最后一公里打包、外置配置与库存对账验证打包这一步直接决定源码能不能变成能用的工具。Maven项目在pom.xml里配maven-assembly-pluginmvn clean package之后得到带依赖的可执行JAR。plugin groupIdorg.apache.maven.plugins/groupId artifactIdmaven-assembly-plugin/artifactId version3.6.0/version configuration archive manifest mainClasscom.whms.MainApp/mainClass /manifest /archive descriptorRefs descriptorRefjar-with-dependencies/descriptorRef /descriptorRefs /configuration executions execution idmake-assembly/id phasepackage/phase goalsgoalsingle/goal/goals /execution /executions /plugin瘦JAR只装自己的class依赖的mysql-connector没打进去换台机器必报ClassNotFoundException。胖JAR把所有依赖合并进同一个jar体积大一点但对桌面交付来说是靠谱的做法。数据库连接外置我习惯放在JAR同级的config目录JdbcUtil启动时先找外部文件、找不到再用内置默认值。配置集中在外面小JAR配不同的数据库就能适应不同部署环境这是桌面交付里性价比最高的一项改造。交付前做一轮库存对账四步验证录一笔入库查库存是否同步增加录一笔出库查库存是否减少故意把出库数量改成大于库存确认系统拦截再跑一次盘点确认差异调整生成一正一负两条库存变动。最后用一条SQL扫一遍负库存mysql -uroot -p -e SELECT goods_id, warehouse_id, quantity FROM stock WHERE quantity 0;返回空结果基本盘就是稳的。我做这类项目有个习惯交付时把建表SQL、初始化账号、运行说明三样东西一并给到对方而不是只丢一个jar。因为对方十有八九不会看代码但一定会按说明去装MySQL、导数据、双击运行。这套流程我栽过跟头有一次只给了jar对方装的是MySQL 5.7驱动类名对不上折腾了半小时才发现是环境版本问题。从那以后我每个Swing项目都附一份环境版本对照说明写明开发验证环境MySQL 8.0.33 JDK 8以上5.7请改用旧驱动类名。希望帮到你。本文还有配套的精品资源点击获取