☰
Java银行管理系统实战:从MySQL建表到Swing界面完整方案
2026/10/9 16:12:26 网站建设 项目流程

简介:这是一套面向Java初学者与课程设计学习者的银行管理系统项目源码,基于IntelliJ IDEA开发,采用Java Swing构建图形界面,以MySQL作为后台数据存储,实现管理员与顾客两类角色的完整业务闭环。管理员可登录、添加或删除顾客、统计存储金额;顾客可完成登录、存款、取款、转账、修改密码、查看交易记录及安全退出,适合作为Java面向对象编程、数据库应用与Swing界面开发的综合练习素材。资源包为rar压缩格式,共28个文件,约40KB,包含14个class编译文件、6个xml与2个yml配置、2个html页面、1个java源码及1个md说明文档,覆盖项目配置、界面资源与核心逻辑,目录结构清晰,便于快速导入IDEA运行调试。目前已有424人学习浏览,读者可借此理解银行系统的模块划分、身份验证与交易处理流程,积累数据库连接、异常处理与界面交互的实战经验,为后续金融类软件开发打下基础。

1. Java 银行管理系统:从建表到 Swing 界面,一套能跑起来的桌面端方案

很多人第一次做 Java 银行管理系统,卡住的地方不是业务逻辑,而是环境。IDEA 装好了,MySQL 也装好了,结果 JDBC 驱动没导进去,或者 Swing 窗口一运行就报NullPointerException。这套系统本质上是一个桌面端 CRUD 应用:JavaSwing 负责界面,MySQL 负责持久化,IDEA 负责开发和调试。它适合两类人——正在找 Java 课程设计题目的学生,以及想用一个小项目把 JDBC、Swing 事件模型、事务处理串起来的初级开发者。核心难点不在代码量,而在三层结构怎么分、连接怎么管、界面和数据库怎么同步。下面按建库、连库、写界面、排错的顺序,把每一步拆开讲。

2. 建库与建表:先把 MySQL 这边的地基打牢

2.1 银行系统需要哪几张表

一个能跑通的银行管理系统,最少需要四张表:用户表、账户表、交易记录表、管理员表。用户表和账户表分开,是因为一个用户可能持有多个账户,这是银行系统的基本模型。交易记录表只增不改,保证流水可追溯。

-- 创建数据库,字符集用 utf8mb4,避免中文乱码 CREATE DATABASE bank_system DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci; USE bank_system; -- 用户表:存客户基本信息 CREATE TABLE t_user ( user_id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL, -- 存 MD5 或 SHA-256,不存明文 real_name VARCHAR(50) NOT NULL, phone VARCHAR(20), create_time DATETIME DEFAULT CURRENT_TIMESTAMP ); -- 账户表:一个用户可以有多个账户 CREATE TABLE t_account ( account_id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, card_no VARCHAR(19) NOT NULL UNIQUE, -- 卡号 balance DECIMAL(15,2) DEFAULT 0.00, -- 金额必须用 DECIMAL status TINYINT DEFAULT 1, -- 1 正常 0 冻结 FOREIGN KEY (user_id) REFERENCES t_user(user_id) ); -- 交易记录表:只插入,不更新 CREATE TABLE t_transaction ( trans_id INT PRIMARY KEY AUTO_INCREMENT, card_no VARCHAR(19) NOT NULL, trans_type VARCHAR(10) NOT NULL, -- 存款/取款/转账 amount DECIMAL(15,2) NOT NULL, trans_time DATETIME DEFAULT CURRENT_TIMESTAMP, remark VARCHAR(100) );

金额字段用DECIMAL(15,2)而不是FLOAT或DOUBLE,这是血泪经验。浮点数在累加时会出现0.1 + 0.2 = 0.30000000000000004这类问题,银行系统里一分钱都不能差。密码字段长度给 64,是因为 SHA-256 输出是 64 位十六进制字符串,如果只存 MD5 给 32 位就够,但建议直接上 SHA-256。

2.2 在 IDEA 里连上 MySQL

打开 IDEA,右侧 Database 面板,点加号选 MySQL。Host 填localhost,Port 填3306,Database 填bank_system。第一次连接会提示下载驱动,点 Download 就行。如果连不上,先确认 MySQL 服务在跑:

# Windows 下检查 MySQL 服务状态 net start | findstr mysql # 如果没启动,手动启动 net start mysql80

连接成功后,IDEA 的 Database 面板能直接看到四张表,后面写代码时字段名不用来回切窗口查。这里有个细节:MySQL 8.0 的驱动类名是com.mysql.cj.jdbc.Driver,5.7 是com.mysql.jdbc.Driver。用错会报ClassNotFoundException,这是新手最常见的翻车点之一。

2.3 JDBC 工具类的写法

不要在每个 DAO 里都写一遍DriverManager.getConnection。抽一个工具类出来,统一管理连接参数。

public class DBUtil { // MySQL 8.0 必须加时区参数,否则报时区错误 private static final String URL = "jdbc:mysql://localhost:3306/bank_system?useSSL=false&serverTimezone=Asia/Shanghai&characterEncoding=utf8"; private static final String USER = "root"; private static final String PASSWORD = "你的密码"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { throw new RuntimeException("MySQL 驱动未导入", e); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } // 关闭资源,顺序不能反:先关 ResultSet,再关 Statement,最后关 Connection public static void close(Connection conn, Statement stmt, ResultSet rs) { try { if (rs != null) rs.close(); } catch (SQLException ignored) {} try { if (stmt != null) stmt.close(); } catch (SQLException ignored) {} try { if (conn != null) conn.close(); } catch (SQLException ignored) {} } }

serverTimezone=Asia/Shanghai这个参数不加,MySQL 8.0 会直接抛The server time zone value '?D1ú±ê×?ê±??' is unrecognized。useSSL=false是本地开发关掉 SSL 警告,生产环境要开。characterEncoding=utf8保证中文能正确写入。

3. Swing 界面:用最少组件搭出可用的操作台

3.1 登录窗口和主窗口怎么分

Swing 的坑在于布局管理器。很多人一上来用绝对定位setBounds,换个分辨率就全乱。正确做法是用BorderLayout做外层,GridBagLayout或GridLayout做表单区。

登录窗口只放三个东西:用户名输入框、密码输入框、登录按钮。主窗口用JTabbedPane分页,把存款、取款、转账、查询流水分成四个 Tab,每个 Tab 里放一个独立的JPanel。

public class LoginFrame extends JFrame { private JTextField userField; private JPasswordField passField; public LoginFrame() { setTitle("银行管理系统 - 登录"); setSize(400, 250); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); // 居中显示 JPanel panel = new JPanel(new GridBagLayout()); GridBagConstraints gbc = new GridBagConstraints(); gbc.insets = new Insets(8, 8, 8, 8); // 组件间距 gbc.gridx = 0; gbc.gridy = 0; panel.add(new JLabel("用户名:"), gbc); gbc.gridx = 1; userField = new JTextField(15); panel.add(userField, gbc); gbc.gridx = 0; gbc.gridy = 1; panel.add(new JLabel("密码:"), gbc); gbc.gridx = 1; passField = new JPasswordField(15); panel.add(passField, gbc); gbc.gridx = 0; gbc.gridy = 2; gbc.gridwidth = 2; JButton loginBtn = new JButton("登录"); loginBtn.addActionListener(e -> doLogin()); panel.add(loginBtn, gbc); add(panel); setVisible(true); } private void doLogin() { String username = userField.getText().trim(); String password = new String(passField.getPassword()); // 这里调 Service 层验证,不要直接写 SQL UserService service = new UserService(); User user = service.login(username, password); if (user != null) { dispose(); // 关掉登录窗 new MainFrame(user); // 打开主窗口 } else { JOptionPane.showMessageDialog(this, "用户名或密码错误"); } } }

setLocationRelativeTo(null)让窗口在屏幕居中,不加这行窗口会跑到左上角。GridBagConstraints的insets控制组件间距,不设的话按钮和输入框会挤在一起。登录逻辑里不要直接写 SQL,调 Service 层,这样后面换数据库或加缓存不用改界面代码。

3.2 存款取款的事件处理与事务

存款和取款的核心是事务。取款要先查余额,够才能扣,扣完要写流水。这三步必须在同一个事务里,否则可能出现扣了钱没写流水的情况。

public class AccountService { public boolean withdraw(String cardNo, BigDecimal amount) { Connection conn = null; PreparedStatement ps = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); // 开启事务 // 第一步:查余额,加行锁防止并发扣款 String querySql = "SELECT balance FROM t_account WHERE card_no = ? FOR UPDATE"; ps = conn.prepareStatement(querySql); ps.setString(1, cardNo); ResultSet rs = ps.executeQuery(); if (!rs.next()) { conn.rollback(); return false; } BigDecimal balance = rs.getBigDecimal("balance"); if (balance.compareTo(amount) < 0) { conn.rollback(); return false; // 余额不足 } // 第二步:扣款 String updateSql = "UPDATE t_account SET balance = balance - ? WHERE card_no = ?"; ps = conn.prepareStatement(updateSql); ps.setBigDecimal(1, amount); ps.setString(2, cardNo); ps.executeUpdate(); // 第三步:写流水 String insertSql = "INSERT INTO t_transaction(card_no, trans_type, amount) VALUES(?, '取款', ?)"; ps = conn.prepareStatement(insertSql); ps.setString(1, cardNo); ps.setBigDecimal(2, amount); ps.executeUpdate(); conn.commit(); return true; } catch (SQLException e) { try { if (conn != null) conn.rollback(); } catch (SQLException ignored) {} e.printStackTrace(); return false; } finally { DBUtil.close(conn, ps, null); } } }

FOR UPDATE是行级锁,防止两个线程同时查到相同余额然后都扣款。setAutoCommit(false)开启事务后,任何一步失败都要rollback。BigDecimal的compareTo返回 -1、0、1,不能用equals,因为equals会比较精度,1.0和1.00不相等。

3.3 表格刷新与界面同步

Swing 的JTable不会自动感知数据库变化。存款成功后,界面上的余额标签和流水表格要手动刷新。

// 刷新流水表格 DefaultTableModel model = (DefaultTableModel) transTable.getModel(); model.setRowCount(0); // 清空旧数据 List<Transaction> list = transService.getByCardNo(cardNo); for (Transaction t : list) { model.addRow(new Object[]{t.getTransTime(), t.getTransType(), t.getAmount(), t.getRemark()}); }

setRowCount(0)清空表格,然后重新查库填充。不要在 Swing 的事件线程里做耗时查询,数据量大时界面会卡死。常见做法是开一个SwingWorker在后台查,查完再publish到表格。小项目数据量小,直接查也能接受,但要知道这个边界。

4. 避坑与排查:那些让项目跑不起来的细节

4.1 驱动导入后仍然报 ClassNotFoundException

现象:IDEA 里已经 Add as Library 了,运行还是报ClassNotFoundException: com.mysql.cj.jdbc.Driver。

原因:jar 包加到了模块依赖,但没加到 Artifact 的输出里。IDEA 运行 Java 应用时,最终用的是编译输出目录,不是模块依赖列表。

解决:File → Project Structure → Artifacts → 选中当前模块 → 在右侧 Available Elements 里双击 mysql-connector-java,把它加到输出根目录下。或者直接用 Maven 管理依赖,在pom.xml里加:

<dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency>

Maven 会自动处理依赖传递和打包,比手动导 jar 省心。

4.2 中文写入数据库变成问号

现象:Swing 界面输入中文姓名,存进 MySQL 后变成???。

原因:三个环节的字符集不一致——数据库、连接 URL、表字段。任何一处不是 utf8,中文就会丢。

解决:建库时指定utf8mb4,连接 URL 加characterEncoding=utf8,表字段用VARCHAR而不是CHAR。如果已经建了表,用ALTER TABLE t_user CONVERT TO CHARACTER SET utf8mb4;改。另外 MySQL 8.0 默认字符集已经是 utf8mb4,5.7 需要手动指定。

4.3 Swing 窗口关闭后进程不退出

现象:点了窗口右上角的 X,窗口关了,但 IDEA 的运行按钮还是红的,进程没结束。

原因:JFrame默认的关闭操作是HIDE_ON_CLOSE,只是隐藏窗口,没有释放资源。如果开了线程或连接池,进程会一直挂着。

解决:setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE)让关闭窗口时退出 JVM。如果有未关闭的数据库连接,在windowClosing事件里手动关掉。更稳妥的做法是加一个WindowAdapter:

addWindowListener(new WindowAdapter() { @Override public void windowClosing(WindowEvent e) { // 关闭连接池等资源 System.exit(0); } });

4.4 转账时两边余额不一致

现象:A 账户扣了钱,B 账户没加钱,或者两边都加了。

原因:转账涉及两条 UPDATE,没有放在同一个事务里,或者事务中途抛异常没有回滚。

解决:转账必须在一个Connection里完成,setAutoCommit(false),两条 UPDATE 都成功后commit,任何一步失败rollback。不要用两个 Service 方法各开各的连接,那样事务控制不住。常见做法是把转账逻辑写在一个方法里,共用同一个Connection。

4.5 IDEA 运行报 cannot start internal http server

现象:IDEA 启动时弹窗报Cannot start internal HTTP server,项目能编译但运行不了。

原因:端口被占用,通常是 63342 端口被其他程序占了,或者 IDEA 的缓存坏了。

解决:先重启 IDEA,不行就 File → Invalidate Caches → Invalidate and Restart。再不行检查 63342 端口:

netstat -ano | findstr 63342

找到占用进程的 PID,在任务管理器里结束掉。这个报错和项目代码无关,是 IDEA 自身的问题,不用改代码。

5. 进阶技巧:用 MyBatis-Plus 反向生成实体类,少写一半代码

手写 DAO 层是体力活。表字段一多,ResultSet的getString、getInt写到手酸。MyBatis-Plus 的代码生成器能根据表结构直接生成实体类、Mapper、Service,省掉大量重复代码。这不是必须的,但用过就回不去了。

在pom.xml里加依赖:

<dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-boot-starter</artifactId> <version>3.5.5</version> </dependency> <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-generator</artifactId> <version>3.5.5</version> </dependency>

写一个生成器类:

public class CodeGenerator { public static void main(String[] args) { FastAutoGenerator.create( "jdbc:mysql://localhost:3306/bank_system?serverTimezone=Asia/Shanghai", "root", "你的密码") .globalConfig(builder -> builder .author("yourname") .outputDir(System.getProperty("user.dir") + "/src/main/java")) .packageConfig(builder -> builder .parent("com.bank") .entity("entity") .mapper("mapper") .service("service")) .strategyConfig(builder -> builder .addInclude("t_user", "t_account", "t_transaction") // 指定表名 .entityBuilder() .enableLombok() // 用 Lombok 省掉 getter/setter .enableTableFieldAnnotation()) .execute(); } }

跑一次main方法,entity、mapper、service三个包自动生成。enableLombok()需要项目里已经引入 Lombok 依赖,否则生成的@Data注解会报错。addInclude指定要生成的表,不写会生成库里所有表,包括你不想要的。

生成后的实体类长这样:

@Data @TableName("t_account") public class Account { @TableId(value = "account_id", type = IdType.AUTO) private Integer accountId; @TableField("user_id") private Integer userId; @TableField("card_no") private String cardNo; @TableField("balance") private BigDecimal balance; @TableField("status") private Integer status; }

@TableName映射表名,@TableId指定主键和自增策略,@TableField映射字段名。有了这些注解,MyBatis-Plus 的BaseMapper直接提供selectById、insert、updateById、deleteById等方法,不用手写 SQL。转账那种复杂事务还是得自己写,但单表 CRUD 基本不用碰 SQL 了。

一个验证技巧:生成完代码后,先写一个测试方法调accountMapper.selectList(null),能查出数据说明映射没问题。查不出来先看表名和字段名是否匹配,MyBatis-Plus 默认开启驼峰转下划线,accountId对应account_id,如果表字段是accountid就会映射失败。

我自己的习惯是:新项目先用生成器把实体和 Mapper 生成出来,再在 Service 层写业务逻辑。这样能把精力放在事务控制和界面交互上,而不是纠结字段名拼写。数据库连接参数和密码不要硬编码在代码里,放到application.properties或环境变量里,这是从第一天就该养成的习惯。希望帮到你。

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

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

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

立即咨询