简介:这是一份面向高校课程设计与毕业设计场景的Java Swing进销存管理系统源码,适合需要学习传统桌面端管理软件开发流程的学生或开发者,围绕企业库存、销售、进货三大业务构建了完整的数据管理与分析功能。压缩包共325个文件,其中75个Java源文件适合阅读核心业务逻辑,169个class文件为编译产物,56张PNG图片展示系统界面,7份Word文档为设计说明,另含SQL Server数据库文件(MDF/LDF)、JAR依赖包等,整体大小约4.47MB,按功能模块组织,便于对照学习。目前已有599人学习下载。资源附带MyEclipse工程配置和SQL Server2000数据库文件,导入后即可运行。系统涵盖信息管理、业务管理、库存管理、查询统计、系统管理五大功能模块,具体包括客户、商品、供应商管理,进货单与销售单处理,库存盘点,以及客户、商品、供应商、销售、入库等多维度查询统计;类设计覆盖系统主窗体、单据操作、数据访问与查询等核心逻辑,既能辅助理解进销存业务流程,也可作为课程设计、毕业设计或Java实训项目的直接参考。
1. 为什么还有人用 Java Swing 写进销存:这套源码到底能干什么
“Java Swing进销存管理系统源码”这个标题,几乎每年都会在 Java 课程设计案例源码的搜索里排进前列。它面向的是一类非常具体的需求:用最传统的 Java 桌面技术,把采购、销售、库存、统计这一条进销存业务链路完整跑通。这套源码的价值不在界面多华丽,而在业务闭环——登录、商品管理、入库出库、库存查询、报表统计,每一环都能对应到数据库里的表和代码里的类。适合三类人:要交课程设计作业的学生、接手公司内部老桌面系统的开发者、以及想用最小依赖理解 Java 业务代码如何组织的初学者。它不搞微服务不搞中间件,但正因为技术选型传统,反而最容易读懂、最容易改。
2. 拿到源码包先做三件事:目录、SQL脚本和入口类的定位
2.1 别急着点运行:先分清 src、sql、docs 三个目录
很多同学拿到压缩包第一反应是解压、导入 IDE、点运行,结果要么报数据库连接失败,要么找不到主类。我一般建议先花十分钟把目录结构摸清楚。一个规范的 Swing 进销存项目,通常长这样:
erp-system/ ├── src/ # Java 源码目录 │ ├── com/erp/ │ │ ├── view/ # 界面类:登录、主窗体、各业务面板 │ │ ├── dao/ # 数据库访问对象 │ │ ├── model/ # 实体类:Goods、Supplier、StockLog 等 │ │ ├── util/ # 数据库连接、MD5、导出等工具 │ │ └── Main.java # 入口类,带 main 方法 ├── sql/ # 数据库初始化脚本 │ └── erp.sql # 建库、建表、插入初始数据 ├── docs/ # 设计文档、数据库说明、使用手册 └── lib/ # 第三方 jar 包,比如 MySQL 驱动先看 docs 里的数据库设计说明,再看 sql 目录下的脚本,最后才看代码,这是最省时间的阅读顺序。如果压缩包里没有显式分目录,那就按包名去定位:com.erp.view是界面层,com.erp.dao是数据访问层,这种分层约定在九成课设项目里都成立。找不到 lib 也不用慌,看代码里import了哪些第三方类,缺什么去对应官网下载即可,最常见的就是 MySQL 的 JDBC 驱动。
2.2 入口类在哪儿:带 main 的类就是起跑线
Swing 程序入口和 Web 项目不一样,它不需要 Tomcat,不需要容器,就是一个普通的main方法。入口类通常叫Main.java、App.java或者LoginFrame.java内部带 main。打开源码文件后,按Ctrl + F12搜public static void main,就能定位到程序起点。
常见做法是入口类只做两件事:初始化一个登录窗口,然后把它显示出来。至于登录之后跳转到主窗口、加载哪个业务模块,都在界面代码里通过事件去驱动。这样设计的用意是让登录框成为整个系统的安全闸门,进销存这种带库存数据和成本信息的系统,直接进主界面是不可接受的。另外注意一点:如果你的 IDE 搜索 main 方法搜出来多个,优先选类名带Login、Main、App的,带Test结尾的基本是单元测试入口,跑错了只会看到控制台输出而不是界面。
2.3 建库建表:进销存最少需要六张表
数据库脚本是整个系统的地基。进销存系统再简化,也绕不开这几张表:用户表(谁在操作)、商品表(卖什么)、库存表(还剩多少)、进货单(进来了什么)、销售单(出去了什么)、库存流水(每一步变化的痕迹)。下面这段建表语句是典型设计,你拿到手可以直接执行:
CREATE DATABASE IF NOT EXISTS erp DEFAULT CHARSET utf8mb4; USE erp; CREATE TABLE sys_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) NOT NULL UNIQUE COMMENT '登录名', password VARCHAR(64) NOT NULL COMMENT 'MD5值', real_name VARCHAR(32) COMMENT '姓名', role VARCHAR(16) DEFAULT 'OPERATOR' COMMENT 'ADMIN/OPERATOR', status TINYINT DEFAULT 1 COMMENT '1启用 0停用' ); CREATE TABLE goods ( id INT PRIMARY KEY AUTO_INCREMENT, code VARCHAR(32) NOT NULL UNIQUE COMMENT '商品编码', name VARCHAR(64) NOT NULL COMMENT '商品名称', spec VARCHAR(64) COMMENT '规格型号', unit VARCHAR(16) COMMENT '单位', price DECIMAL(10,2) NOT NULL DEFAULT 0 COMMENT '参考售价', stock INT NOT NULL DEFAULT 0 COMMENT '当前库存', min_stock INT DEFAULT 0 COMMENT '库存下限预警', status TINYINT DEFAULT 1 ); CREATE TABLE stock_log ( id INT PRIMARY KEY AUTO_INCREMENT, goods_id INT NOT NULL, type TINYINT NOT NULL COMMENT '1入库 2出库 3盘点调整', quantity INT NOT NULL, before_stock INT NOT NULL, after_stock INT NOT NULL, biz_no VARCHAR(32) COMMENT '关联业务单号', operate_user VARCHAR(32), operate_time DATETIME DEFAULT CURRENT_TIMESTAMP );utf8mb4是必须的,不然商品名称里有生僻字或者 emoji 会直接报错;DECIMAL而不是float存金额,这是财务数据的基本底线;stock_log表记录每次操作前后的库存快照,是排查库存差异和做审计的重要依据。原项目如果只有五张表,通常是把进货单和销售单合并成了一张biz_order表,用type字段区分类型,也能跑通,但你后续写报表会麻烦一些。
3. 登录到主窗口:Swing 事件机制和分层是怎么配合的
3.1 数据库连接工具类:直连好还是连接池好
Swing 进销存程序里最常见的数据库连接做法是 JDBC 直连,原因很简单:这类系统通常是小规模局域网内使用,并发量低,C3P0、Druid 这些连接池在这里属于杀鸡用牛刀。但直连也有讲究,不能每个按钮事件里都写一遍DriverManager.getConnection,那样代码会散落一地。一个规范的工具类长这样:
public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/erp" + "?useUnicode=true&characterEncoding=utf8" + "&useSSL=false&serverTimezone=Asia/Shanghai"; private static final String USER = "root"; private static final String PWD = "123456"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new ExceptionInInitializerError(e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PWD); } }注意几个参数:characterEncoding=utf8是为了让中文正常读写,缺失它你会在查询时看到一堆问号;useSSL=false是关闭 MySQL 8.x 默认的 SSL 校验,不写会多一段握手警告;serverTimezone指定时区,MySQL 8 没它会报时区错误。密码直接写在代码里确实不安全,但课设和内部系统这么干的占绝大多数,如果你要做更正规的交付,可以把连接信息放到config.properties文件里,代码用Properties类去读取,这样换数据库环境不用改代码重新编译。
3.2 登录验证的代码骨架:校验、加盐和会话
登录按钮是进销存系统的第一个事件入口。新手常犯的错误是校验逻辑写得太薄——文本框非空就放行,这是把界面校验当成了安全认证。一个合格的登录代码要实现三层:输入校验、凭证比对、会话记录。下面这段是骨架,注意它是写在登录窗体的按钮事件里的:
btnLogin.addActionListener(e -> { String username = txtUser.getText().trim(); String password = new String(txtPwd.getPassword()); // 第一层:输入校验 if (username.isEmpty() || password.isEmpty()) { JOptionPane.showMessageDialog(this, "用户名和密码不能为空"); return; } // 第二层:数据库比对,密码采用MD5存储 String sql = "SELECT id, real_name, role FROM sys_user " + "WHERE username = ? AND password = ? AND status = 1"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setString(1, username); ps.setString(2, Md5Util.md5(password)); try (ResultSet rs = ps.executeQuery()) { if (rs.next()) { // 第三层:记录会话,然后跳转主窗口 UserContext.setLoginUser(new User(rs.getInt("id"), rs.getString("real_name"), rs.getString("role"))); new MainFrame().setVisible(true); dispose(); } else { JOptionPane.showMessageDialog(this, "用户名或密码错误"); } } } catch (SQLException ex) { ex.printStackTrace(); JOptionPane.showMessageDialog(this, "数据库连接失败,请检查服务"); } });这里有几个细节值得说明。第一,密码框用getPassword()返回的是char[],而不是字符串,这是 Swing 的设计,字符数组用完后可以立刻清空,减少内存中被反射拿到密码的风险,所以千万别为省事改成getText();第二,SQL 里的status = 1是一个很容易被忽略的过滤条件,它能让你在不停用账号的情况下禁止其登录;第三,UserContext是一个很简单的静态类,用ThreadLocal或者直接静态变量保存当前登录用户,主窗体里的所有业务操作都能拿到操作人姓名,这是后面库存流水表operate_user字段的数据来源。
3.3 主界面用 JTabbedPane 装下七个业务页
登录成功之后进入主窗体,大部分进销存系统的主界面结构是左侧功能菜单配右侧内容区,但 Swing 项目里更常见、也更省事的布局是用JTabbedPane做顶部页签。这样做的好处是新窗口不用一个个弹,登录一次后所有功能都在一个窗口内切换,而且代码组织起来清晰——每个页签对应一个JPanel子类。
public class MainFrame extends JFrame { public MainFrame() { setTitle("进销存管理系统 - 当前用户:" + UserContext.getLoginUser().getRealName()); setSize(1024, 700); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); JTabbedPane tabs = new JTabbedPane(); tabs.addTab("商品管理", new GoodsPanel()); tabs.addTab("入库管理", new StockInPanel()); tabs.addTab("出库管理", new StockOutPanel()); tabs.addTab("库存查询", new StockQueryPanel()); tabs.addTab("报表统计", new ReportPanel()); tabs.addTab("供应商管理", new SupplierPanel()); tabs.addTab("系统设置", new SettingsPanel()); setContentPane(tabs); } }标题栏带上当前登录用户的名字,是一种很实用的操作反馈,多人在一台机器上轮流操作时一眼能看出当前是谁在使用。JTabbedPane的切换事件有时间开销吗?几乎没有,因为它默认只加载当前页,但要注意如果你的业务面板构造方法里就去查数据库,那么主窗口一打开就会执行七个面板的数据库查询,启动会明显变慢。解决办法是把数据加载放到面板的updateUI或者首次显示时再触发,这是后面避坑章节要重点讲的性能问题。
4. 库存和出入库的三个核心代码场景
4.1 商品表格数据绑定:AbstractTableModel 的正确用法
进销存界面里最常出现的组件是表格,商品列表、入库记录、出库记录全是表格。新手容易把数据一个格子一个格子塞进DefaultTableModel,这样不仅代码臃肿,而且商品实体改个字段就要同步改表格代码。常见的做法是自定义一个继承AbstractTableModel的模型类,把你的实体List直接放进去:
public class GoodsTableModel extends AbstractTableModel { private String[] columns = {"商品编码", "名称", "规格", "单位", "售价", "库存", "状态"}; private List<Goods> list; public void setGoodsList(List<Goods> list) { this.list = list; fireTableDataChanged(); // 通知表格重新渲染 } @Override public int getRowCount() { return list == null ? 0 : list.size(); } @Override public int getColumnCount() { return columns.length; } @Override public String getColumnName(int column) { return columns[column]; } @Override public Object getValueAt(int rowIndex, int columnIndex) { Goods g = list.get(rowIndex); switch (columnIndex) { case 0: return g.getCode(); case 1: return g.getName(); case 2: return g.getSpec(); case 3: return g.getUnit(); case 4: return g.getPrice(); case 5: return g.getStock(); case 6: return g.getStatus() == 1 ? "在售" : "停用"; default: return null; } } }使用的时候,查询完数据库直接把结果丢给模型:
GoodsTableModel model = new GoodsTableModel(); JTable table = new JTable(model); // 查询后 model.setGoodsList(dao.findAll());fireTableDataChanged()是本段代码的灵魂。没有这行,你改了list的数据,表格界面上纹丝不动,看起来就像数据没查进去。为什么不直接在表格上操作?因为 Swing 的表格是可重用的视图组件,数据模型和界面显示解耦之后,同一个模型可以对应多种展示方式,打印报表时也能复用这个模型的数据。等后续要加渲染逻辑时,比如库存低于下限的红色标亮,在你的模型里重写getForeground或者getBackground方法就行,不用动任何业务代码。
4.2 出入库必须走事务:库存表和流水表一个都不能少
出入库是进销存里逻辑最重的操作,它本质上要完成三件事:记录进货单或销售单、修改goods表的库存字段、在stock_log表里留一条流水。这三件事只要其中一件失败,整个操作就是半途而废。比如扣了库存但没写流水,下个月对账就怎么都对不上。所以事务是必须的:
public boolean stockOut(StockOutDTO dto) { Connection conn = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 1. 查库存并校验是否足够 String checkSql = "SELECT stock FROM goods WHERE id = ?"; int stock; try (PreparedStatement ps = conn.prepareStatement(checkSql)) { ps.setInt(1, dto.getGoodsId()); try (ResultSet rs = ps.executeQuery()) { if (!rs.next()) throw new RuntimeException("商品不存在"); stock = rs.getInt("stock"); if (stock < dto.getQuantity()) { throw new RuntimeException("库存不足,当前仅剩 " + stock); } } } // 2. 更新库存 String updateSql = "UPDATE goods SET stock = stock - ? WHERE id = ?"; try (PreparedStatement ps = conn.prepareStatement(updateSql)) { ps.setInt(1, dto.getQuantity()); ps.setInt(2, dto.getGoodsId()); ps.executeUpdate(); } // 3. 写流水 String logSql = "INSERT INTO stock_log " + "(goods_id, type, quantity, before_stock, after_stock, biz_no, operate_user) " + "VALUES (?, 2, ?, ?, ?, ?, ?)"; try (PreparedStatement ps = conn.prepareStatement(logSql)) { ps.setInt(1, dto.getGoodsId()); ps.setInt(2, dto.getQuantity()); ps.setInt(3, stock); ps.setInt(4, stock - dto.getQuantity()); ps.setString(5, dto.getBizNo()); ps.setString(6, UserContext.getLoginUser().getRealName()); ps.executeUpdate(); } conn.commit(); // 全部成功才提交 return true; } catch (Exception e) { if (conn != null) { try { conn.rollback(); } catch (SQLException ignored) {} } throw new RuntimeException("出库失败:" + e.getMessage(), e); } finally { if (conn != null) { try { conn.setAutoCommit(true); } catch (SQLException ignored) {} try { conn.close(); } catch (SQLException ignored) {} } } }这段代码要注意两点。一是setAutoCommit(false)必须在任何 SQL 执行前调用,如果你先执行了一条查询,再关掉自动提交,那之前的操作已经独立提交了;二是finally里要把autoCommit恢复为true,因为连接池或下次复用时不会主动重置它。before_stock和after_stock的设计是这套方案里最有价值的细节——它让每一次库存变化的可追溯性变得非常强,出了问题可以直接对比流水里的前后值,快速定位到是哪个环节多扣了或少加了。
4.3 条件查询拼 SQL:PreparedStatement 参数化是底线
进销存的查询功能有一个共同套路:有一组可选的条件输入框,用户填了某个条件就按某个字段过滤,不填就查全部。很多人在这一步会用字符串拼接直接写 SQL,这是最危险的习惯。我之前接手过一个项目,查询代码里赫然写着"WHERE name = '" + keyword + "'"这种语句,一旦有人输入' OR '1'='1就能绕过全部分页逻辑,这种系统对外上线就是裸奔。参数化的写法明明更简洁:
public List<Goods> searchGoods(String name, Integer status) { StringBuilder sql = new StringBuilder("SELECT * FROM goods WHERE 1 = 1 "); List<Object> params = new ArrayList<>(); if (name != null && !name.trim().isEmpty()) { sql.append("AND name LIKE ? "); params.add("%" + name.trim() + "%"); } if (status != null) { sql.append("AND status = ? "); params.add(status); } sql.append("ORDER BY id DESC"); List<Goods> result = new ArrayList<>(); try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql.toString())) { for (int i = 0; i < params.size(); i++) { ps.setObject(i + 1, params.get(i)); } try (ResultSet rs = ps.executeQuery()) { while (rs.next()) { result.add(mapToGoods(rs)); } } return result; } catch (SQLException e) { throw new RuntimeException("查询商品失败", e); } }WHERE 1 = 1看着像个笨拙的技巧,但它让后面每一个AND拼接都变得安全——不用判断当前是不是第一个条件,不用考虑加AND还是WHERE。setObject可以省去逐个判断参数类型的麻烦,它内部会根据PreparedStatement的元数据做适配。这里想强调的是,模糊查询的%要和参数一起绑定,而不是拼进 SQL 字符串,原因依然是防止特殊字符注入。参数化这条底线守住,配合LIMIT分页,一个桌面系统的查询层就足够稳了。
5. 进销存系统避坑指南:六个常见翻车现场与解决
5.1 界面假死:在 EDT 上跑 JDBC,卡到怀疑人生
现象:点击“查询”按钮后,整个窗口变成白色无响应状态,任务栏显示“未响应”,过几秒甚至几十秒才恢复。
原因:Swing 的界面刷新和事件响应都在 EDT(Event Dispatch Thread)线程上执行,你把数据库连接、查询、遍历结果集这些耗时操作全放在按钮事件里,等于在唯一能刷界面的线程上做阻塞,界面自然卡死。数据量大、网络慢的时候尤其致命。
解决:耗时操作用SwingWorker丢到后台线程执行,查询期间显示进度条或禁用按钮,完成后再切回 EDT 更新界面。最常见的最小改动是:
btnSearch.addActionListener(e -> { btnSearch.setEnabled(false); new SwingWorker<List<Goods>, Void>() { @Override protected List<Goods> doInBackground() { return goodsDao.searchGoods(txtName.getText(), null); } @Override protected void done() { try { model.setGoodsList(get()); btnSearch.setEnabled(true); } catch (Exception ex) { JOptionPane.showMessageDialog(null, "查询失败:" + ex.getMessage()); } } }.execute(); });UI 操作一律只在done()里做,别在doInBackground()里碰任何组件。记住一句话:数据库可以慢,界面永远不能等。
5.2 中文乱码:编码不一致的连锁反应
现象:登录界面中文正常,查询出来的商品名称是????或者æžœæ±�这类乱码。
原因:乱码问题通常是三个环节里至少有一个编码不一致——Java 源文件编译编码(IDE 保存编码)、JDBC 连接串的字符集、数据库表的默认字符集。常见组合是源码保存为 UTF-8,连接串没指定字符集,数据库表建的是 latin1,三处对不上自然乱码。
解决:把三个环节统一为一种编码,推荐全程 UTF-8。IDE 里把项目编码、文件编码全部设为 UTF-8;数据库连接串加上characterEncoding=utf8;建库时显式指定默认字符集,建好的表可以用ALTER TABLE goods CONVERT TO CHARACTER SET utf8mb4修正。改完后重启数据库连接,如果还是乱码,检查原库里的数据是不是已经损坏,是的话只能重新导入 SQL 脚本。
5.3 表格不刷新:忘了 fireTableDataChanged
现象:入库操作成功后,切回商品管理页,库存数字还是旧的,必须重新打开窗口才能看到更新。
原因:数据源更新了,但表格模型没有通知视图重绘。JTable不会自动感知List里的变化,它完全依赖模型层的通知机制。用的是自定义AbstractTableModel,就必须在数据变更后手动调用fireTableDataChanged()或者更细粒度的fireTableRowsUpdated()。
解决:在GoodsTableModel.setGoodsList()末尾加fireTableDataChanged(),这是治本的改法,所有调用它更新数据的地方都会自动刷新。如果项目用的是DefaultTableModel,更新数据时要调用setDataVector()或setRowCount(0)再重新addRow(),完事同样要记得触发。
5.4 金额计算翻车:double 精度问题
现象:进货 3 件商品单价 19.9 元,总价算出 59.699999999999996,打印出来用户看着想砸电脑。
原因:double是二进制浮点数,无法精确表示 19.9、0.1 这种十进制小数,这是硬伤,跟算法无关。
解决:金额字段类型改成BigDecimal,数据库字段用DECIMAL(10,2)匹配。计算时统一用BigDecimal的add、multiply、subtract方法,构造时用字符串而不是 double:new BigDecimal("19.9")。如果你是从ResultSet里拿getBigDecimal("price"),那本身就是BigDecimal,直接用即可。千万别图省事在BigDecimal和double之间来回转,转一次精度就丢一次。
5.5 点关闭窗口进程不退出:隐藏的 AWT 线程
现象:关闭登录窗或主窗口后程序窗口消失了,但 IDE 控制台里的红色方块还在,说明 JVM 没退出。
原因:setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)没有生效,或者代码里手动创建的线程(定时备份、后台任务)没有停止。尤其是如果登录后只dispose()了登录窗口,而程序的主入口线程还在等待,JVM 就不会退出。
解决:入口类的main方法里对主窗口设置EXIT_ON_CLOSE,并在WindowClosing事件里加打印日志,确认关闭事件确实被触发。如果你写了java.util.Timer做定时任务,记得关闭时调用cancel(),否则那个 timer 线程会永远挂住 JVM。
5.6 设计文档和代码对不上:文档是上上版
现象:按文档里的数据库说明建表,导入代码后运行报字段不存在。
原因:课设项目常见通病,文档写于开发前一版或开发过程中改了设计但忘了同步文档,代码才是活的,文档是死的。
解决:以源码为准,doc 为参考。跑不起来先看 sql 目录下的脚本是不是最新、有没有被二次修改过;如果 sql 脚本按文档建表,但查询代码里引用了文档里没有的字段,说明代码和脚本脱节了,优先改数据库脚本适配代码。真正做交付时,建议反过来——进销存这类系统,文档里的数据库设计应该是根,代码跟着文档走。如果是别人写的代码你只是接手维护,那只能先信代码,再把文档修正到跟代码一致,别省这一步,不然下个人继续踩这个坑。
6. 交付前最后一步:打 jar 包、写启动脚本、留一条日志后路
进销存系统写到能跑只是第一步,能交付给使用者双击运行才是完整的闭环。开发环境里点 IDE 绿色按钮运行程序,别人电脑上没有你的 JDK 配置,可能根本启动不了。常见做法是打一个可执行 jar,配合一个.bat或.vbs启动脚本,把环境配置细节全部封装掉。
# 编译产物在 out/ 目录下,lib 里有 mysql-connector jar cfm erp.jar MANIFEST.MF -C out . -C lib .MANIFEST.MF里声明主类和类路径:
Main-Class: com.erp.Main Class-Path: lib/mysql-connector-j-8.0.33.jar启动脚本可以用最直接的start.bat:
@echo off javaw -Xms256m -Xmx512m -jar erp.jar用javaw而不是java是为了不弹出黑色控制台窗口。如果使用者机器上没配过 PATH,脚本里显式写死 JDK 路径更稳妥:"C:\Java\jdk-17\bin\javaw.exe"。交付前一定要在没装 IDE 的机器或虚拟机里实测一次,这一步能暴露大量环境问题。另外一个容易被忽略的点是日志——写一个简单的LogThread在关闭时把当天的操作记录到本地logs/目录,排障和交接时这是后悔药级别的好习惯。
进销存这个方向真正练人的地方,不在用没用过高级框架,而在事务、状态、边界条件这些基本功。我见过太多面试时能背 java 八股文的人,一说库存扣减和流水一致性就露馅。这套 Swing 进销存源码,你把它从头到尾改一遍、跑一遍、再亲手部署给模拟用户用一次,比看十篇架构文章都实在。希望帮到你。
本文还有配套的精品资源,点击获取