☰
Python实战:基于Tkinter与SQLite的个人财务管理系统
2026/10/22 6:14:12 网站建设 项目流程

最近刚做完一个基于Python的个人财务管理系统,源码、数据库、文档全部整理完毕。这套系统解决的问题特别直接:让你搞清楚每一笔钱从哪来、花到哪去。我之前用Excel记账,时间一长工作表又乱又多,做透视表折腾得头疼。现在换成本地Python程序,双击就能跑,不需要部署什么Web服务。系统基于Python 3.10开发,数据库选的SQLite,界面用的是Tkinter,额外接了Matplotlib做图表。功能上覆盖收支流水录入、多级分类管理、月度预算监控、年度报表统计,还带分类占比和月度趋势可视化。不管你是刚开始学Python的开发者,还是想拿它当课程设计参考,又或者只是单纯想要一个顺手好用的个人记账工具,这套源码加上数据库脚本和项目文档,完全可以拿来直接改、直接用。下面我把整个设计思路、核心实现和踩过的坑完整拆开讲一遍。

1. 项目概述与整体设计

1.1 核心需求拆解

个人财务管理系统的核心,不是把账记下来,而是让人能看懂钱花到了哪里。我在需求分析阶段把用户痛点归纳成三点:第一,日常流水记录效率低,手机语音记一笔再手动录入表格很浪费时间;第二,月底统计分类汇总太痛苦,得过一次透视表依赖症;第三,预算一旦做出来就没人跟进,超支完全无感。基于这些痛点,我把系统功能拆成了五个核心模块:账户管理、收支记录、分类管理、预算管理、统计报表。每个模块都坚持“够用不臃肿”的原则,不堆砌冗余功能,比如不做复杂的股票基金资产估值,不做多币种换算,先把日常收支这件事做扎实。

从需求层级上看,个人单机用户最在意的是数据的归属权和私密性。所以系统设计成纯本地存储,数据库文件直接放在用户自己的电脑上,不联网、不注册、不绑定手机号。这样既满足了个人记账的数据安全心理预期,也规避了一堆部署问题。考虑到后续可能会交给别人使用,我还在登录模块加入了简单的用户鉴权逻辑,密码采用哈希存储,不同用户拥有独立的数据空间,这个设计虽然简单,但结构上是完整的。

系统对运行环境的要求也不高。Windows、macOS、Linux都能跑,唯一需要的就是Python 3.8以上解释器和一个Matplotlib依赖库。这样的轻量定位,让任何人拿到源码之后都能在五分钟内跑起来,不会因为环境配置劝退。

1.2 技术选型分析

技术选型的核心原则是“能用就用,不给自己加戏”。整个项目只用了一个第三方库,数据可视化用的Matplotlib,其余全部基于Python标准库。

技术组件选型选择理由
开发语言Python 3.10语法简洁,标准库丰富,适合桌面工具快速迭代
GUI框架TkinterPython自带,无需额外安装,td code风格成熟
数据库SQLite轻量级单机数据库,文件即数据,备份方便
可视化Matplotlib支持折线图、饼图,能与Tkinter无缝嵌入
数据导出CSV标准库csv实现,表格软件通用
依赖管理pip + venv环境隔离,避免版本冲突

为什么不用Django或者Flask做Web版?因为个人财务数据属于高隐私数据,放到浏览器里一旦忘记部署安全策略,反而容易暴露。桌面版虽然不能随时随地访问,但数据只在本机文件里,安全边界非常清晰。而且对于大多数课程设计和个人练手项目,Tkinter已经足够完成所有交互需求,再套一个重型Web框架属于过度设计。

SQLite还有一个隐藏优势:它不用单独安装数据库服务,创建连接就是打开一个文件。这样在联网环境切换、U盘拷贝、打包成exe时,都省去了数据库配置步骤。后续如果真要改成MySQL,主要改连接驱动,SQL语法层面95%可以复用,迁移成本很低。

1.3 系统架构与模块划分

系统整体采用MVC思想的简化版,分为界面层、业务控制层、数据访问层。界面层只负责事件响应和组件渲染,不直接写SQL;业务控制层处理记录新增、统计报表等逻辑;数据访问层封装所有SQLite操作。这样分层的收益很大:后期如果把Tkinter界面换成PyQt,改动面只停留在views目录;如果想把数据源从SQLite换成MySQL,只需要重写models和database目录下的连接逻辑。

项目目录结构参考:

finance_app/ ├── main.py # 程序入口 ├── requirements.txt # 依赖清单 ├── database/ │ ├── db_helper.py # 数据库连接与初始化 │ └── finance.db # SQLite数据库文件(自动生成) ├── models/ │ ├── user.py # 用户模型 │ ├── category.py # 分类模型 │ └── transaction.py # 交易记录模型 ├── controllers/ │ ├── auth_controller.py # 登录注册控制 │ └── finance_controller.py # 记账统计控制 ├── views/ │ ├── login_view.py # 登录窗口 │ ├── main_view.py # 主界面 │ └── report_view.py # 报表视图 ├── utils/ │ ├── date_utils.py # 日期处理工具 │ └── export_utils.py # 导出工具 └── docs/ └── 项目文档.md # 完整项目文档

这个结构对新手来说可能觉得文件夹多,但实际写下来会发现每个文件都很短。把一个大项目拆成小文件的好处是改起来不慌。比如想要加一个“债务管理”模块,我只需要新增债务模型、修改主界面加一个标签页、在控制器里加一组函数,不碰其他文件。这种模块化思维,比写一个两千行的单文件脚本要健康得多。

2. 数据库设计与核心表结构

2.1 SQLite 还是 MySQL

很多课程设计和初学者项目一上来就上MySQL,实际上个人单机系统完全没必要。SQLite和MySQL的核心差异在于运行形态:MySQL是独立的网络数据库服务,需要安装、启动服务、建库建用户;SQLite则是嵌入式数据库,你的程序就是它的主人,一个文件就是整个数据库。

选择SQLite的另一个原因是备份极简单。系统自动记录每天的数据文件副本,拷贝走finance.db就等于备份了全部账目。MySQL备份要导出SQL文件或者备份数据目录,对于个人用户太复杂。SQLite的单写多读模式对于单用户记账绰绰有余,最大支持TB级数据量,日常账目记录百万条也不会成为瓶颈。

当然,如果项目定位就是Web版多人协作,那就得换MySQL或PostgreSQL,从SQLite切换过去的重点是检查两点:一是日期函数语法差异,SQLite用strftime,MySQL用DATE_FORMAT;二是自增主键写法,SQLite使用AUTOINCREMENT,MySQL使用AUTO_INCREMENT。在项目初期把这些差异控制在一个数据库访问层里,后面迁移会轻松很多。

2.2 核心表设计

个人财务管理系统最重要的几张表是:用户表、分类表、账户表、收支记录表、预算表。我最终设计如下:

-- 用户表 CREATE TABLE users ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password_hash TEXT NOT NULL, created_at TEXT DEFAULT (datetime('now', 'localtime')) ); -- 账户表 CREATE TABLE accounts ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, account_name TEXT NOT NULL, account_type TEXT NOT NULL, -- 'cash', 'bank_card', 'e_wallet' balance REAL DEFAULT 0, created_at TEXT DEFAULT (datetime('now', 'localtime')), FOREIGN KEY (user_id) REFERENCES users(id) ); -- 分类表 CREATE TABLE categories ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, parent_id INTEGER DEFAULT 0, -- 0表示一级分类 name TEXT NOT NULL, type TEXT NOT NULL, -- 'income' 或 'expense' sort_order INTEGER DEFAULT 0 ); -- 收支记录表 CREATE TABLE transactions ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, account_id INTEGER NOT NULL, category_id INTEGER NOT NULL, transaction_date TEXT NOT NULL, -- 格式 YYYY-MM-DD amount REAL NOT NULL, note TEXT, created_at TEXT DEFAULT (datetime('now', 'localtime')), updated_at TEXT DEFAULT (datetime('now', 'localtime')), FOREIGN KEY (user_id) REFERENCES users(id), FOREIGN KEY (account_id) REFERENCES accounts(id), FOREIGN KEY (category_id) REFERENCES categories(id) ); -- 预算表 CREATE TABLE budgets ( id INTEGER PRIMARY KEY AUTOINCREMENT, user_id INTEGER NOT NULL, category_id INTEGER, budget_month TEXT NOT NULL, -- 格式 YYYY-MM budget_amount REAL NOT NULL, created_at TEXT DEFAULT (datetime('now', 'localtime')), UNIQUE(user_id, category_id, budget_month) );

字段类型的取舍我踩过一个坑。刚开始amount字段直接用了REAL,后来计算月度汇总时发现浮点数误差累计会显示成0.30000000000000004。所以金额字段现在虽然定义为REAL,但业务层强制以“分为单位”的整数进行运算,展示时才除以100。这笔账一定要在源头算明白,否则后面看到稀奇古怪的数字还要回头排查。

日期字段我统一用文本格式存储,格式固定为YYYY-MM-DD。SQLite的日期函数对这个格式支持得最舒服,strftime能直接做月份聚合。不要为了省事存时间戳,时间戳在报表分组时需要多一层转换,纯属自找麻烦。

2.3 关键统计SQL逻辑

统计功能是整套系统的灵魂。月度收支趋势图的数据来源是这条SQL:

SELECT strftime('%Y-%m', transaction_date) AS month, SUM(CASE WHEN t.amount >= 0 THEN t.amount ELSE 0 END) AS total_income, SUM(CASE WHEN t.amount < 0 THEN ABS(t.amount) ELSE 0 END) AS total_expense FROM transactions t WHERE t.user_id = ? AND t.transaction_date >= ? GROUP BY strftime('%Y-%m', transaction_date) ORDER BY month;

我用了正负号区分收入和支出,收入存正数,支出存负数。这样在写SQL时不需要JOIN分类表去判断类型,性能更快,而且流水列表展示时非常直观。分类占比饼图的数据来自另一条SQL:

SELECT c.name, SUM(CASE WHEN t.amount < 0 THEN ABS(t.amount) ELSE 0 END) AS total FROM transactions t JOIN categories c ON t.category_id = c.id WHERE t.user_id = ? AND t.transaction_date LIKE ? AND c.type = 'expense' GROUP BY c.name ORDER BY total DESC;

LIKE '2025-04%'这种方式虽然能用,效率不高,但本地几万条数据根本无感。代码里我会先把月份转成起止日期再用range条件,严谨一些。预算剩余金额的查询要注意LEFT JOIN,因为某个月份可能还没给该分类设置预算,但也要显示实际支出。

3. 系统实现与关键代码

3.1 环境准备与项目初始化

从零开始复现这套系统,环境部分只需要四步。首先安装Python 3.10或更高版本;然后创建虚拟环境,Windows下面命令是python -m venv venv,之后激活并安装依赖。这里我只提供一份最小化依赖清单,requirements.txt里面写matplotlib==3.8.4即可,Tkinter和sqlite3都是Python标准库,不需要装。

初始化数据库时建议把建表脚本和初始化分类数据都封装在db_helper.py里。第一次运行自动执行建表语句,再插入预设的默认分类,比如餐饮、交通、购物、工资、理财收益等。用户拿到源码直接启动就能看到分类下拉框有数据,体验会好很多。我习惯在每个建表语句前加IF NOT EXISTS,避免重复运行时报错。

# database/db_helper.py 核心片段 import sqlite3 DB_PATH = 'database/finance.db' def get_connection(): conn = sqlite3.connect(DB_PATH) conn.row_factory = sqlite3.Row conn.execute("PRAGMA foreign_keys = ON") return conn def init_db(): conn = get_connection() cursor = conn.cursor() cursor.executescript( """ CREATE TABLE IF NOT EXISTS users (...); CREATE TABLE IF NOT EXISTS accounts (...); -- 其他建表语句 """ ) conn.commit() conn.close()

设置row_factory = sqlite3.Row非常关键。这样查询结果可以直接用row["username"]方式读取,而不是默认的元组下标方式,代码可读性完全不同。PRAGMA foreign_keys = ON是SQLite开启外键约束的开关,默认关闭的,不打开会导致删分类时把关联记录全部变成孤儿数据。

3.2 用户认证与安全处理

密码存储直接用了hashlib做加盐哈希。虽然bcrypt这类库更专业,但标准库方案零依赖更省事。登录逻辑不复杂:用户输入用户名和密码,从数据库取出哈希值再比对。重点在注册时要加盐,盐值就是注册时随机生成的字符串,和哈希结果一起存进数据库。

import hashlib, os, hmac def hash_password(password: str) -> str: salt = os.urandom(16).hex() digest = hashlib.pbkdf2_hmac('sha256', password.encode(), bytes.fromhex(salt), 100000) return f"{salt}${digest.hex()}" def verify_password(password: str, stored: str) -> bool: salt, digest = stored.split('$') verify_digest = hashlib.pbkdf2_hmac('sha256', password.encode(), bytes.fromhex(salt), 100000) return hmac.compare_digest(verify_digest.hex(), digest)

pbkdf2_hmac是标准库里的密码派生函数,迭代次数10万次,安全性足够个人项目使用。注意不要用md5这种已经不适合做密码哈希的算法。hmac.compare_digest用来做哈希比对可以防御计时攻击,属于写进代码就一劳永逸的操作。

3.3 收支记录与分类管理界面

主窗口用ttk.Notebook做成标签页,页签分别是“流水账”“分类管理”“预算设置”“统计报表”。流水页左侧是录入表单:账户下拉框、分类下拉框、金额输入框、日期选择器、备注输入框,右侧是Treeview表格展示当月记录。

Treeview的列定义有一个细节容易被忽略:所有列都必须显式设置anchor和width,否则不同系统下显示效果差异很大。日期列我用了tkcalendar的DateEntry控件,这个控件比较友好,但为了减少依赖也可以用StringVar配合Entry,输入格式由代码校验兜底。

录入表单提交时要把金额字符串转换为整数分:

def parse_amount_to_cents(raw: str) -> int: try: amount_float = float(raw) if amount_float <= 0: raise ValueError("金额必须大于0") return round(amount_float * 100) except ValueError: raise

支出时业务层再把整数分转成负数存入amount字段。这种以分存储的设计避开浮点陷阱,报表层显示出来也稳定。录入成功后立刻刷新Treeview,同时重新计算底部“当月总收入/总支出/结余”三个Label,让用户马上看到结果,反馈感很重要。

分类管理页用两列联动:左侧是一级分类列表,右侧显示该分类下的子分类。分类数据在系统初始化时预置,但允许用户自行添加和修改。添加分类时一定要校验type字段,收入分类和支出分类不能混在一起,否则统计图会错乱。

3.4 数据可视化与报表导出

统计报表页嵌入了两个Matplotlib图表:月度收支趋势折线图和分类支出占比饼图。Tkinter嵌入Matplotlib需要用到FigureCanvasTkAgg,这个类在matplotlib.backends.backend_tkagg模块里。每次刷新图表前要清除旧图,否则内存会被反复叠图撑爆。

from matplotlib.backends.backend_tkagg import FigureCanvasTkAgg from matplotlib.figure import Figure def refresh_trend_chart(self, data_list): self.trend_ax.clear() if not data_list: self.trend_canvas.draw() return months = [item['month'] for item in data_list] incomes = [item['total_income'] / 100 for item in data_list] expenses = [item['total_expense'] / 100 for item in data_list] self.trend_ax.plot(months, incomes, label='收入', marker='o') self.trend_ax.plot(months, expenses, label='支出', marker='o') self.trend_ax.legend() self.trend_ax.set_ylabel('金额') self.trend_ax.grid(True) self.trend_canvas.draw()

图表刷新的卡顿问题是性能优化里的重点。我一开始每次刷新都是self.fig.clear()再重画,内部对象全部销毁重建,窗口在连续点击月份切换时能明显感觉到掉帧。后来改成在每个Figure上只调用ax.clear(),复用Figure对象和Canvas,再设置plt.rcParams['figure.dpi'] = 100,流畅度提升明显。

导出功能我用csv模块实现,月度流水一键导出成CSV,然后用Excel打开就能二次分析。CSV文件最怕中文乱码,写文件时需要设置encoding='utf-8-sig',这个编码会带BOM头,Excel识别得最稳。导出金额时统一除以100得到可读的元单位。

4. 源码之外的文档与打包发布

4.1 合格的项目文档应该写什么

很多人做项目只写代码,最后要交文档时临时拼凑。我自己吃过这个亏,所以这次特意在开发过程中同步记录了文档。一份个人财务管理系统项目文档,至少需要包含需求分析、系统设计、数据库设计、功能实现说明、测试报告、使用说明六个部分。

需求分析不需要写得特别高大上,重点是“用户故事”。比如“作为普通用户,我希望记账时能直接选分类,而不是手动打字”,这样的描述比“系统应提供分类选择功能”更容易指导开发。数据库设计部分要把建表SQL、表关系说明、字段含义整理成表格,别人接盘时不至于对着字段发怵。

使用说明最好配截图,登录页截图、主页流水截图、图表页截图各安排一张。文档里还要写清楚运行环境、启动命令、测试账号。如果涉及打包发布,要把打包步骤和常见问题一起写进去。课程设计或毕业设计评审时,文档的完成度往往比系统复杂程度更重要,这算是过来人的经验。

4.2 PyInstaller 打包成免环境应用

源码发给别人,别人不一定装了Python。用PyInstaller打包成exe可以解决这个痛点。打包命令特别简单:

pip install pyinstaller pyinstaller -w -F main.py --name FinanceApp --collect-all matplotlib

-w表示不显示命令行窗口,-F表示打包成单文件。不加--collect-all matplotlib的话,打包出来的exe运行时大概率报错找不到Matplotlib数据文件。还有一个Windows专属坑是tkinter样式问题,有些字体在打包环境中显示异常,最好在代码里把全局字体设置成微软雅黑这类常见字体。

数据库文件的路径问题尤其要注意。如果用相对路径“database/finance.db”,PyInstaller打包后的exe工作目录是临时解压目录,每台机器解压路径都不一样,意味着数据文件可能落在临时目录里,退出后数据就没了。解决方案是在代码中把数据路径固定到用户目录:

import os from pathlib import Path def get_app_data_dir() -> str: data_dir = os.path.join(str(Path.home()), ".finance_app") os.makedirs(data_dir, exist_ok=True) return data_dir

将DB_PATH改为app_data_dir加上finance.db,源码运行时数据放在用户目录,不再污染项目目录,打包后也能正常持久化。这个改动属于一次解决未来所有打包路径问题的根本方案。

5. 常见问题与排查技巧

5.1 数据库误删与备份恢复

个人财务管理系统的数据都是真金白银的记录,误删之后很难找回。开发时系统里应该内置备份机制,我采用的是每日自动备份方案。每次程序启动时,检查数据库文件最近修改时间,如果比昨天的备份新,就把文件复制成finance_YYYYMMDD.db备份。建议只保留最近30个备份文件,超过就自动清理,避免磁盘空间被撑爆。

还有一种场景是用户自己误操作删掉某条流水。我提供的兜底方案是在流水表上增加deleted标记字段,不真正DELETE,只在界面上标记为已删除并过滤显示。设计上复杂了一点,但对账时能找回记录,这个决定相当值。

5.2 金额精度和日期格式坑

金额用浮点存储是个大坑,前文已经讲了分部整数运算。日期格式最常见报错是知乎上问烂了的“strptime time data does not match format”,根本原因是用户输入了不规范的日期。解决方式是在控件层就限制输入,日期选择控件禁止键入,员下拉方式选择。如果要用手动输入,一定要先正则匹配YYYY-MM-DD格式,再调用datetime.strptime校验。

统计报表按月查询时,如果用户选择了没有数据的月份,图表区域记得显示“暂无数据”而不是画一张空图。这个判断虽然只是几行代码,但实际使用体验差距很大,空白的画布会让人误以为系统死机了。

5.3 数据库锁与并发问题

SQLite默认是文件级锁,单用户单进程下自然没感觉。但如果程序里存在多个窗口同时操作数据库的路径,比如报表线程刷新图表时主线程插入新流水,就可能出现“database is locked”错误。解决方式是控制数据库连接生命周期,统一用单例连接,或者每次操作创建一个短连接并设置timeout=10。

我的做法是尽量避免多线程操作数据库。Matplotlib图表刷新其实可以只在主线程完成,不用开后台线程,因为个人记账的数据量很小,SQL查询都是毫秒级。如果未来数据量大,再考虑使用concurrent.futures线程池,并给每个线程单独的数据库连接。

5.4 常见报错速查表

报错信息原因解决方案
sqlite3.OperationalError: no such table数据库未初始化检查init_db()是否在启动入口执行
AttributeError: 'NoneType' object has no attribute 'get'查询结果为空时用了row["字段"]先用row是否为None做判断
ModuleNotFoundError: matplotlib.backends.backend_tkaggMatplotlib版本异常pip uninstall matplotlib再重装官方源版本
TypeError: strptime() argument 1 must be str日期传入的是datetime对象统一使用str格式化后再比较
TclError: no display name and no DISPLAY environment variable在无窗口Linux环境运行Tkinter需要桌面环境或改用Xvfb

排查问题时我习惯在代码里加详细的日志。标准logging模块配置一下,每次SQL执行都记录操作时间、操作类型、用户ID,出问题后翻日志比重新复现操作要快很多。

6. 个人实操心得与扩展方向

6.1 交付与维护心得

整套系统从零开始写到最终交付,最深的体会是“结构比技巧重要”。最早版本我把所有代码塞进一个main.py,总共一千多行,改一个功能要滚动半小时才找到位置。重构后按MVC分层,各文件变小,定位问题反而更快。写代码不能光想着自己看得懂,还要考虑一周后的自己和接手的同学能不能看懂。

测试数据一定要单独造一套,不要拿自己的真实账目做演示。我准备了一套模拟数据,包含两年内每月50条随机流水,覆盖工资、报销、餐饮、购物、旅行等分类。这套数据既用来测试统计图表,也可以放进使用说明的截图中,不会泄露隐私。

文档里还应该写清楚项目已知的限制。我在文档中注明:“本系统定位为本地个人财务工具,不支持多人多端实时同步;没有做云端加密存储;不可用于团队账目管理。”主动声明边界反而让人更信任,比藏着掖着被问出来要好。

6.2 下一步可以扩展的五个方向

第一个方向是导入银行账单。支付宝和微信支付都支持导出CSV账单,写一个解析器把账单CSV映射到系统表结构,可以免去手工录账的痛苦。这个扩展能解决记账工具最大的留存痛点:手动记录坚持不下去。

第二个方向是模糊分类建议。根据历史交易备注自动推断分类,比如备注里包含“外卖”就自动归入餐饮。规则引擎非常简单,一个关键词映射表就能跑起来,再进阶一点还可以用机器学习做分类预测,但这对于单机项目可能溢出需求。

第三个方向是账单周期提醒。对接系统内置的每月固定支出比如房租、房贷、信用卡还款,用webbrowser模块打开网页或者用系统通知弹窗做提醒。不需要SDK,纯标准库就可以实现。

第四个方向是把系统改成Web版,采用Flask做API服务,前端用Vue或原生HTML. 数据库直接从SQLite替换成PostgreSQL,复用现有models逻辑,大部分代码能保留。多用户并发的场景下,预算和分类表需要加更多的索引,SQL查询要重新梳理。

第五个方向是手机端适配。最简单的方式不是写App,而是做一个局域网内可访问的Web页面,手机上用浏览器直接记账。这需要把Tkinter层整个替换掉,但业务层和数据层完全不动,这也是当时坚持分层的回报。

最后再分享一个小技巧:开发这类管理系统时,一定要用git做版本管理,每完成一个功能提交一次。我中途因为想试一个界面效果,把主窗口布局改坏了,git checkout一键还原,省了很多重写时间。个人项目也许没有协作者,但版本管理带来的安全感是不可替代的。

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

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

立即咨询