最近刚做完一个基于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用strftimeMySQL用DATE_FORMAT二是自增主键写法SQLite使用AUTOINCREMENTMySQL使用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逻辑统计功能是整套系统的灵魂。月度收支趋势图的数据来源是这条SQLSELECT 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分类表去判断类型性能更快而且流水列表展示时非常直观。分类占比饼图的数据来自另一条SQLSELECT 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里面写matplotlib3.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收入, markero) self.trend_ax.plot(months, expenses, label支出, markero) 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文件最怕中文乱码写文件时需要设置encodingutf-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_okTrue) 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”错误。解决方式是控制数据库连接生命周期统一用单例连接或者每次操作创建一个短连接并设置timeout10。我的做法是尽量避免多线程操作数据库。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一键还原省了很多重写时间。个人项目也许没有协作者但版本管理带来的安全感是不可替代的。
阅读完成 · 觉得有帮助?