简介:这是一套基于Java Swing开发、采用C/S结构的个人财务收支管理系统源码,整体围绕eclipse/Myeclipse与MySQL数据库构建,面向Java初学者、课程设计及毕业设计场景。系统覆盖用户管理、类别管理、收入管理、支出管理、个人信息修改、密码修改、登录与退出等模块,可清晰演示Swing窗体布局、按钮事件处理、JDBC数据库连接与增删改查操作,适合用来理解桌面应用从界面到数据层的完整开发链路。资源包共111个文件,包括22个Java源文件、80个class编译文件、5个jar依赖库、1个SQL数据库脚本以及项目配置文件,压缩包仅2.91MB,结构简洁,便于导入开发环境快速运行。系统初始账号为admin/admin,数据库脚本为javacaiwu.sql,可在此基础上二次开发或学习调试。目前已有207人学习浏览,适合需要可运行项目源码并对照学习Swing+MySQL开发的读者。
1. 一套Java Swing个人财务系统源码,先搞清楚它解决什么问题
期末课程设计、毕设,或者想补Java桌面开发的空,最容易碰到的一类题就是“个人财务收支管理系统”。这类系统用Java Swing做界面,Eclipse做开发环境,MySQL存数据,C/S结构——客户端负责交互,数据库负责持久化。你拿到源码后先别急着跑,它的核心价值不在界面多好看,而在于把JDBC连接、表结构设计、增删改查、统计报表这一套完整串起来。适合想快速跑通一个桌面项目、准备答辩演示的人往下读;如果你只想抄界面,那不如直接去看现成的JavaFX项目更省事,Swing的优势恰恰在简单直接、依赖少、跑起来不挑机器。
2. 环境与工程骨架:Eclipse + JDBC + MySQL 的选型和组织方式
这种CS结构的Swing项目,最常见的源码组织方式是Eclipse里的一个Java Project,按util(数据库连接)、ui(窗口)、dao(数据访问)、model(实体)、service(业务)分包。拿到源码第一件事不是双击运行,而是先看包结构和lib目录,搞清楚驱动JAR放没放、连接串配在哪,再谈跑起来。
2.1 版本组合怎么选:JDK 8、MySQL 5.7/8.0 与连接驱动的坑
个人财务系统是典型的轻量桌面应用,JDK 8完全够用,Eclipse我用2020年之后的版本配JDK 8最省心。数据库端常见两种组合,决定了后面驱动和连接串的写法:
| 组合 | 驱动类名 | 连接串是否带serverTimezone | 适用场景 |
|---|---|---|---|
| MySQL 5.7 + mysql-connector-java 5.1.49 | com.mysql.jdbc.Driver | 可不带 | 老机器、旧课设源码 |
| MySQL 8.0 + mysql-connector-java 8.0.33 | com.mysql.cj.jdbc.Driver | 必须带 | 新装环境、新写项目 |
我一般建议直接上MySQL 8.0加配套8.x驱动,因为官网默认下载就是8.x,用老驱动连新版数据库会触发SSL握手报错,处理起来更绕。JAR包放进项目的lib目录,然后在Eclipse里选中lib目录,右键Build Path -> Add to Build Path。这步漏了,编译时IDE不会直接报错,运行到Class.forName那行才会抛ClassNotFoundException,新手容易卡很久。
选型时还要注意,Eclipse的Run配置里JRE要选JDK而不是纯JRE,否则Swing类库加载不全。新手跑这个标题的源码,大概率栽在环境组合上,先把这层理清,后面代码本身反而不难。
2.2 源码导入Eclipse:Package结构、Build Path和Run配置
拿到源码压缩包,先解压看有没有.classpath和.project这两个文件。有的话,Eclipse里File -> Import -> Existing Projects into Workspace,选中解压目录直接导入;没有的话,新建一个Java Project,把src目录整个拷过去,再把lib下的JAR手动加回Build Path。
跑之前确认两件事。第一,项目的JRE选的是JDK而不是纯JRE,Swing类库在JDK里才完整。第二,Run Configuration的Main class指向带main方法的入口类,Swing项目通常叫LoginUI或MainFrame,别选成某个测试类。这两步做完,绝大多数“导入后跑不起来”的问题就消失了。
不少源码压缩包里还带一个finance.sql这样的初始化文件。用Navicat或者mysql命令行执行一遍,库和表就建好了,不用自己手敲SQL。执行前先打开看一眼字符集,如果CREATE DATABASE后面没写DEFAULT CHARACTER SET,你自己补上utf8mb4,免得后面中文乱码。然后看源码的包分布,常见的分层是model放实体、dao放SQL操作、service放业务逻辑、ui放Swing界面。有些课设源码会把所有setVisible和addActionListener全堆在一个类里,这种代码能跑,但加功能很难受;拿到这种源码,我一般不动整体结构,只在ui包下加新面板。
2.3 建库建表:收支两张表的最小设计
个人财务系统最核心的就两张表:用户表t_user和流水表t_record。流水表是重头,字段不要贪多,能支撑“谁、什么时间、收支类型、分类、金额、备注”就够了。SQL如下:
CREATE DATABASE IF NOT EXISTS finance DEFAULT CHARACTER SET utf8mb4; USE finance; CREATE TABLE IF NOT EXISTS t_user ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL UNIQUE, password VARCHAR(64) NOT NULL ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE IF NOT EXISTS t_record ( id INT PRIMARY KEY AUTO_INCREMENT, user_id INT NOT NULL, type TINYINT NOT NULL COMMENT '0支出 1收入', category VARCHAR(20) NOT NULL, amount DECIMAL(10,2) NOT NULL, remark VARCHAR(200), record_date DATE NOT NULL, create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, KEY idx_user_date (user_id, record_date), FOREIGN KEY (user_id) REFERENCES t_user(id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;这里几个参数值得展开。amount用DECIMAL(10,2)而不是double,金额经不起浮点误差,10位有效数字、2位小数对个人记账绰绰有余。type用TINYINT比VARCHAR省空间,也方便在Java端做映射,0是支出,1是收入。record_date用DATE只存“哪一天”的账,不掺时间,统计报表按天分组会简单很多。create_time是系统落库时间,和记账日期分开,避免用户补录昨天的账时把时间搞乱。KEY idx_user_date是给月统计查询准备的联合索引,数据量一大,GROUP BY就指着它走。
2.4 DBUtil连接工具类:连接串、驱动、时区参数一次配好
连接工具类每个窗口都要用,写一个静态方法返回Connection,代码放util包:
package util; import java.sql.Connection; import java.sql.DriverManager; import java.sql.SQLException; public class DBUtil { private static final String URL = "jdbc:mysql://localhost:3306/finance" + "?useUnicode=true&characterEncoding=utf8&serverTimezone=Asia/Shanghai"; private static final String USER = "root"; private static final String PASSWORD = "123456"; static { try { Class.forName("com.mysql.cj.jdbc.Driver"); } catch (ClassNotFoundException e) { e.printStackTrace(); } } public static Connection getConnection() throws SQLException { return DriverManager.getConnection(URL, USER, PASSWORD); } }静态块里Class.forName的目的是让DriverManager注册驱动类,MySQL 8的驱动类名是com.mysql.cj.jdbc.Driver,5.x是com.mysql.jdbc.Driver,这里最容易看错。URL里的useUnicode=true和characterEncoding=utf8保证中文读写正常,serverTimezone=Asia/Shanghai配合MySQL 8的时区校验;如果用的是5.1.49旧驱动,这一项可以不写。
账号和密码直接写在常量里,学习项目没问题,真要答辩或者工作里展示,建议改成读取配置文件,源码一旦分享出去,数据库口令就跟着泄露了。拿到这4个包,写第一行代码之前,建议先验证DBUtil能不能通:写一个带main的临时类,调用DBUtil.getConnection()并打印conn.isClosed()。返回false说明驱动、URL、账号密码、MySQL服务四项都没问题,再接Swing界面就等于接了个普通Java类,没有网络和数据库变量在捣乱。这一步能筛掉后面八成“界面点登录没反应”的诡异问题。
3. Swing界面落地:登录、主窗口、记账与列表展示
Swing项目的界面代码量通常占一半以上,但真正考验人的不是布局,而是组件之间的数据联动:登录成功要跳主窗口,主窗口里切Tab要刷新不同面板,点保存后表格要立即能看到新记录。这一章按从登录到列表的顺序,把四个关键界面的写法和联动原理讲清楚。
3.1 登录窗口:事件监听与用户校验
登录窗口是所有功能的大门。结构上就是一个JFrame,上面放两个文本框、一个密码框、一个登录按钮。关键代码在按钮的ActionListener里:
public class LoginUI extends JFrame { private JTextField userField = new JTextField(15); private JPasswordField passField = new JPasswordField(15); public LoginUI() { setTitle("个人财务收支管理系统 - 登录"); setSize(360, 220); setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE); setLocationRelativeTo(null); JPanel panel = new JPanel(new GridBagLayout()); panel.add(new JLabel("用户名"), gbc(0, 0)); panel.add(userField, gbc(1, 0)); panel.add(new JLabel("密码"), gbc(0, 1)); panel.add(passField, gbc(1, 1)); JButton loginBtn = new JButton("登录"); loginBtn.addActionListener(e -> { String username = userField.getText().trim(); String password = new String(passField.getPassword()); if (new UserDao().validate(username, password)) { new MainUI(username).setVisible(true); dispose(); } else { JOptionPane.showMessageDialog(this, "用户名或密码错误"); } }); panel.add(loginBtn, gbc(1, 2)); setContentPane(panel); } // gbc(int x, int y) 是返回GridBagConstraints的辅助方法 }两个细节值得说。第一,密码框用JPasswordField,取值时要new String(passField.getPassword()),而不是getText(),这是Java对安全的老传统,拿到的char[]用完要尽快丢。第二,userField.getText().trim()把首尾空格去掉,避免用户手滑多敲空格导致登录失败。validate方法里SQL是SELECT COUNT(*) FROM t_user WHERE username=? AND password=?,密码在入库时建议做SHA-256摘要,别用明文。
实际项目里登录逻辑还会加“锁定窗口”或“连续失败次数限制”,课设源码一般没有。答辩时想多讲两句,可以在validate返回false时统计失败次数,同一用户名连续错3次就禁用登录按钮30秒,这个细节成本低、说得出原理。
3.2 主窗口布局:菜单栏、工具栏与卡片式切换
登录成功后的主窗口,常见做法是JFrame加JTabbedPane,把“记一笔”“明细查询”“统计报表”三个面板并排。JTabbedPane的好处是不用自己管理面板切换,点Tab就自动显示对应内容:
public class MainUI extends JFrame { private String username; public MainUI(String username) { this.username = username; setTitle("个人财务收支管理系统 - 当前用户: " + username); setSize(900, 600); setLocationRelativeTo(null); JMenuBar menuBar = new JMenuBar(); JMenu menu = new JMenu("系统"); JMenuItem exitItem = new JMenuItem("退出"); exitItem.addActionListener(e -> System.exit(0)); menu.add(exitItem); menuBar.add(menu); setJMenuBar(menuBar); JTabbedPane tabs = new JTabbedPane(); tabs.addTab("记一笔", new RecordPanel(username)); tabs.addTab("明细查询", new QueryPanel(username)); tabs.addTab("统计报表", new ReportPanel(username)); setContentPane(tabs); } }这里的重点是setTitle里带上当前用户名,功能简单但答辩时很显眼,说明你考虑了多用户隔离。三个面板构造函数都接收username,是为了让每个面板的SQL都带WHERE user_id=?,避免A用户看到B用户的账。很多课设源码犯的错就是把user_id写死成1,换个账号登录,数据全是同一个人,这种问题在演示时一戳就破。
3.3 记账表单:金额校验与分类下拉
记账面板是系统的数据入口。布局上用JPanel配合GridBagLayout或简单的GridLayout,放类型下拉、分类下拉、金额输入框、日期输入框、备注文本域和保存按钮。类型和分类之间要做联动:
typeCombo.addActionListener(e -> { boolean isIncome = "收入".equals(typeCombo.getSelectedItem()); categoryCombo.removeAllItems(); List<String> categories = isIncome ? Arrays.asList("工资", "奖金", "理财", "其他") : Arrays.asList("餐饮", "交通", "购物", "居住", "娱乐", "医疗", "其他"); for (String c : categories) { categoryCombo.addItem(c); } });分类数据另一种做法是建一张t_category表,在数据库里维护,适合想扩展的源码。如果你拿到的源码把分类写死在Java数组里,也能用,但改分类得重新编译。保存按钮的校验逻辑至少要有两层:金额是不是合法的数字,日期格式是不是yyyy-MM-dd。用BigDecimal的构造方法就能拦掉非法输入:
saveBtn.addActionListener(e -> { try { BigDecimal amount = new BigDecimal(amountField.getText().trim()); if (amount.compareTo(BigDecimal.ZERO) <= 0) { JOptionPane.showMessageDialog(this, "金额必须大于0"); return; } LocalDate date = LocalDate.parse(dateField.getText().trim()); // 组装Record对象,调用RecordDao.insert() JOptionPane.showMessageDialog(this, "保存成功"); clearForm(); } catch (NumberFormatException ex) { JOptionPane.showMessageDialog(this, "金额格式不正确"); } catch (DateTimeParseException ex) { JOptionPane.showMessageDialog(this, "日期格式应为yyyy-MM-dd"); } });用BigDecimal接收金额输入,是顺手解决浮点精度问题的好习惯。金额位数超过两位、或者用户输入负数,都在前端拦掉,数据库里的DECIMAL(10,2)再兜一层底。日期不弹日历选择器的话,可以直接在JTextField里按yyyy-MM-dd输入;想体验好点,用JDateChooser这个第三方组件,但课设阶段没必要引入额外jar依赖。
3.4 JTable动态刷新:TableModel与渲染器
明细查询面板的核心是JTable。JTable本身只管显示,数据要喂给TableModel。最简单的写法是用DefaultTableModel,查完数据重新setModel一次,界面就会刷新:
public void loadData(int userId) { List<Record> list = new RecordDao().listByUser(userId); String[] columns = {"日期", "类型", "分类", "金额", "备注"}; Object[][] data = new Object[list.size()][5]; for (int i = 0; i < list.size(); i++) { Record r = list.get(i); data[i][0] = r.getRecordDate().toString(); data[i][1] = r.getType() == 0 ? "支出" : "收入"; data[i][2] = r.getCategory(); data[i][3] = r.getAmount(); data[i][4] = r.getRemark(); } table.setModel(new DefaultTableModel(data, columns)); }这套写法的关键是每次刷新都构建新的DefaultTableModel。如果只改了底层数据而不调setModel,表格永远显示旧数据,这是Swing新手最常见的“按钮点了没反应”。另一个加分项是给金额列加自定义渲染器,让支出显示红色、收入显示绿色:
table.getColumnModel().getColumn(3).setCellRenderer( new DefaultTableCellRenderer() { @Override public Component getTableCellRendererComponent(JTable table, Object value, boolean isSelected, boolean hasFocus, int row, int column) { Component c = super.getTableCellRendererComponent( table, value, isSelected, hasFocus, row, column); Object type = table.getValueAt(row, 1); if ("支出".equals(type)) { c.setForeground(new Color(0xCC0000)); } else { c.setForeground(new Color(0x006600)); } return c; } } );渲染器是JTable进阶里最实用的技巧。getValueAt(row, 1)取的是当前行第二列的“类型”值,所以颜色判断不会错位。这段代码不加也不影响功能,但加了之后演示效果会有明显提升,评审看到彩色收入和支出,比看一片黑字更容易留下印象。
4. 业务逻辑与统计:SQL聚合、日期范围和金额精度
界面跑通之后,系统的骨架就有了。但“个人财务”的关键在“统计”两个字。这一章讲三块:按月收支统计怎么写SQL,分类汇总怎么为报表准备数据,以及多笔记录批量入库时如何用事务保证一致性。
4.1 月收入支出的聚合SQL写法
按月统计,SQL用DATE_FORMAT + GROUP BY + SUM,一句搞定:
SELECT DATE_FORMAT(record_date, '%Y-%m') AS month, type, SUM(amount) AS total FROM t_record WHERE user_id = ? GROUP BY DATE_FORMAT(record_date, '%Y-%m'), type ORDER BY month DESC;返回结果里month是“2026-01”这种字符串,type是0或1。在DAO层把它们封装成一个MonthStat对象(字段month、type、total),再在界面里把收入和支出分别取出来,就能画一个简单的月收支柱状图。这里最容易踩的坑是:直接在SQL里用MONTH(record_date)而不是DATE_FORMAT,那样跨年会串数据——1月的账会把2025年和2026年的一起统计进去。所以宁可让SQL多写几个字符,也要带上%Y。
DAO层的写法可以配合try-with-resources,省去手动关连接的样板代码:
public List<MonthStat> sumByMonth(int userId) throws SQLException { String sql = "SELECT DATE_FORMAT(record_date, '%Y-%m') AS month, type, SUM(amount) AS total " + "FROM t_record WHERE user_id = ? " + "GROUP BY DATE_FORMAT(record_date, '%Y-%m'), type ORDER BY month DESC"; try (Connection conn = DBUtil.getConnection(); PreparedStatement ps = conn.prepareStatement(sql)) { ps.setInt(1, userId); try (ResultSet rs = ps.executeQuery()) { List<MonthStat> list = new ArrayList<>(); while (rs.next()) { MonthStat stat = new MonthStat(); stat.setMonth(rs.getString("month")); stat.setType(rs.getInt("type")); stat.setTotal(rs.getBigDecimal("total")); list.add(stat); } return list; } } }GROUP BY左边和SELECT左边必须一致,MySQL虽然默认开启了ONLY_FULL_GROUP_BY,但实际老版本可能放宽,写出SELECT * GROUP BY type这种语句也能跑出脏数据。统计场景里,SELECT只保留分组字段和聚合函数,是底线。
4.2 分类汇总与报表数据准备
分类汇总回答的是“这个月钱都花在哪了”。SQL按category分组,再按金额倒序:
SELECT category, SUM(amount) AS total FROM t_record WHERE user_id = ? AND type = 0 AND record_date BETWEEN ? AND ? GROUP BY category ORDER BY total DESC;两个参数分别传这个月的第一天和最后一天。算第一天可以用LocalDate.now().with(TemporalAdjusters.firstDayOfMonth()),最后一天是with(TemporalAdjusters.lastDayOfMonth()),不要用“01”和“31”写死,因为2月不是31天。把结果填进一个JTable或者组合成饼图的Dataset,就是一个典型的收支构成展示。排序用ORDER BY total DESC,这样表格第一行永远是花得最多的分类,不用在Java里再排一遍。
如果拿到的源码里统计查询是查出全部记录后在Java内存里for循环累加,数据量小也能跑。但表里有了几千条流水后,内存累加会拖慢界面,SQL聚合是更稳的做法。这也是“源码能不能改造”的关键分水岭:统计逻辑留在SQL里,界面代码就不用动。
类别统计的结果除了填JTable,也可以直接组装成饼图的dataset。JFreeChart是Java里最常用的图表库,从官网下载jfreechart和jcommon两个jar放进lib,用PieDataset接收CategoryStat列表,ChartFactory.newPieChart(...)生成ChartPanel放进统计面板,代码量不大。但图表只是同一份统计结果的另一种呈现,先保证表格里的数字是对的,再谈画图。
4.3 事务处理:批量记账不丢数据
批量记账是给“一键导入历史流水”这类功能准备的。多个INSERT要么全成功,要么全回滚。JDBC的默认提交模式是每执行一条就commit,所以批量插入前要关掉自动提交:
public void batchInsert(List<Record> records) throws SQLException { Connection conn = null; PreparedStatement ps = null; try { conn = DBUtil.getConnection(); conn.setAutoCommit(false); String sql = "INSERT INTO t_record(user_id, type, category, amount, remark, record_date)" + " VALUES(?,?,?,?,?,?)"; ps = conn.prepareStatement(sql); for (Record r : records) { ps.setInt(1, r.getUserId()); ps.setInt(2, r.getType()); ps.setString(3, r.getCategory()); ps.setBigDecimal(4, r.getAmount()); ps.setString(5, r.getRemark()); ps.setDate(6, java.sql.Date.valueOf(r.getRecordDate())); ps.addBatch(); } ps.executeBatch(); conn.commit(); } catch (SQLException e) { if (conn != null) { conn.rollback(); } throw e; } finally { if (ps != null) { ps.close(); } if (conn != null) { conn.setAutoCommit(true); conn.close(); } } }这段代码的核心是setAutoCommit(false)和最后的commit、rollback配对。addBatch把SQL攒在驱动里,executeBatch一次性发给MySQL,比循环executeUpdate快一个数量级,几千条数据也是秒级。finally里把autoCommit恢复成true是给连接池准备的,虽然这里用的是DriverManager直连,但养成这个习惯,将来换连接池就不会留坑。
注意setDate的第二参数要求java.sql.Date,不能用java.time.LocalDate直接传,需要valueOf转换。这个转换漏了,编译直接报类型不兼容。事务的范围只包insert,查询操作不进事务,这个边界别搞混。批量导入的入口可以放在系统菜单里,选一个文本文件,一行一条流水,解析成List 后调batchInsert,整批进来或者整批回滚,不会出现导了一半发现重复数据的情况。
5. 避坑/常见问题/排查:Swing + MySQL 项目里最容易翻车的5个点
这一章把我在类似项目里反复遇到的5个问题按“现象、原因、解决”写清楚。这些问题不解决,功能写得再好都跑不起来。
5.1 现象:MySQL 8连接报时区错误
现象:项目第一次启动,登录窗口没弹出来,控制台抛SQLNonTransientConnectionException,提示The server time zone value 'xxx' is unrecognized。
原因:MySQL 8的时区校验比5.7严格,连接串里没有serverTimezone参数,驱动就无法跟服务器对齐时区。
解决:在DBUtil的URL末尾加上serverTimezone=Asia/Shanghai,或者改成serverTimezone=GMT%2B8。前者更推荐,因为能处理夏令时。如果驱动是5.x,换成mysql-connector-java 8.0.33,类名同步改成com.mysql.cj.jdbc.Driver。
5.2 现象:中文乱码
现象:程序跑通后,界面输入中文保存到数据库没问题,但表格或者导出的CSV里中文全是问号。
原因:三个环节任一环节没对齐——数据库表字符集不是utf8mb4、JDBC连接串没带characterEncoding=utf8、导出CSV时用了系统默认编码。
解决:按顺序排查。建库SQL加DEFAULT CHARACTER SET utf8mb4;连接串加useUnicode=true&characterEncoding=utf8;导出CSV时用OutputStreamWriter(new FileOutputStream(...), StandardCharsets.UTF_8)。如果CSV是要给Excel打开的,还要在文件开头写一个BOM头(EF BB BF),否则Excel会当成ANSI解析导致中文乱码。这一个BOM的坑,我在导出功能上翻过两次车。
5.3 现象:JTable修改后不刷新
现象:在“记一笔”面板保存一条新记录,切到“明细查询”面板,表格里看不到刚记的账。
原因:JTable绑定的是旧的DefaultTableModel实例,数据源变了但模型没换,表格自然不更新。Swing不会自动感知数据库变化。
解决:在明细查询面板的构造方法里、以及每次从其它Tab切回来时,都调用loadData()重新setModel。最省事的方案是在MainUI里给JTabbedPane加一个ChangeListener,Tab切换到明细查询时就刷新一次。另一个更贴合的方案是保存成功后直接调用queryPanel.loadData()主动刷新。两种都行,推荐后者,代码路径更明确。
5.4 现象:eclipse找不到或无法加载主类
现象:右键运行项目,Eclipse弹框提示“找不到或无法加载主类”,控制台有时还会出现org.apache.catalina.startup.Bootstrap。
原因:这个工程是从Web项目改过来的,或者Eclipse的Run Configuration残留了旧配置,项目里混入了Tomcat相关的Classpath条目。
解决:Project Properties -> Java Build Path -> Libraries,把Tomcat相关的条目Remove掉,确认没有引用错误。然后右键项目 -> Maven -> Update Project(如果是Maven工程),或者Project -> Clean。最后检查Run Configuration,把Main class重新选成LoginUI。纯Java桌面项目不依赖Tomcat容器,看到Bootstrap就要清理。
5.5 现象:金额计算出现0.30000000000000004
现象:记一笔“买早饭3元,买咖啡3元”,统计支出显示6.000000000000001。
原因:Java的double是二进制浮点,无法精确表示0.1、0.3这类十进制小数,累加后误差累积就露出来了。
解决:金额字段全链路用BigDecimal,数据库列用DECIMAL(10,2),Java端接收用BigDecimal,前端输入校验也走BigDecimal构造。加法不要直接add到double上,而是声明一个BigDecimal total,循环add。这条解决了,财务报表的数字才敢拿给人看。
6. 进阶:给系统加数据备份与CSV导出,让源码真正可用
6.1 用mysqldump做每日备份
个人财务系统跑起来后,最怕的不是界面丑,是数据库文件坏了或者误删。MySQL自带mysqldump,一行命令就能把整个finance库导出成SQL文件:
mysqldump -u root -p finance > finance_backup_$(date +%Y%m%d).sqlWindows上没有date命令,可以写在批处理里用%date:~0,4%%date:~5,2%%date:~8,2%拼日期。恢复的时候执行mysql -u root -p finance < finance_backup_20260101.sql。mysqldump导出的是完整建库建表语句加INSERT,比手动复制data目录靠谱,因为后者在MySQL运行状态下拷贝容易得到损坏的快照。这个脚本放到计划任务里,每天凌晨跑一次,账就不会白记。
6.2 导出CSV
CSV导出是用户最常要的功能,把明细表按日期倒序导出到文件:
try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter( new FileOutputStream("records.csv"), StandardCharsets.UTF_8))) { writer.write("\uFEFF"); // UTF-8 BOM,让Excel正确识别中文 writer.write("日期,类型,分类,金额,备注\n"); for (Record r : list) { writer.write(String.join(",", r.getRecordDate().toString(), r.getType() == 0 ? "支出" : "收入", r.getCategory(), r.getAmount().toPlainString(), r.getRemark()) + "\n"); } }注意三件事。第一,UTF-8 BOM是为了Excel用Windows默认编码打开时中文不乱码。第二,金额用toPlainString()而不是toString(),因为BigDecimal.toString在科学计数法场景会输出带E的字符串,Excel里会显示成奇怪的数字。第三,备注里如果含逗号,要用双引号包起来,上面这段为了简洁没处理,真要上线得加个escape函数。导出菜单挂在系统菜单栏里,选完路径就写文件,功能虽小,却是从“能跑”走向“能用”的一步。
我自己的习惯是,凡是涉及MySQL的桌面项目,源码交付前一定把备份和导出这两个功能补上,哪怕界面糙一点。数据没备份,演示现场电脑死机一次,几个月的记录就全没了。这段逻辑不复杂,但能让你这套源码从“能跑”变成“敢用”。希望帮到你。
本文还有配套的精品资源,点击获取