1. 个人财务管理系统到底在解决什么问题1.1 记账的本质数据采集加分类汇总先说我的真实感受市面上记账软件一抓一大把但真正能坚持用超过三个月的没几个。问题往往不出在你懒而是软件的设计跟你的使用习惯不匹配。不是太复杂就是太简单数据还动不动同步失败。我当时做这套基于 Python Vue 的个人财务管理系统就是想解决几个非常具体的问题第一我的收支流水得有一个地方统一记录第二月底做总结时能按分类看这个月在餐饮、交通、购物上分别花了多少第三数据必须在自己手里导出、备份、二次分析都不受限制。从本质上看个人财务管理系统就是一个“数据采集 分类汇总 趋势分析”的工具。你把日常的每一笔收入和支出记下来系统按时间、分类、账户维度做汇总再通过报表和图表把趋势呈现出来。很多人以为记账的核心是“记录”这个动作其实记录只是第一步分类和统计才是决定这个工具有没有价值的关键。这也是为什么我在设计数据库表结构时把分类表单独拆了出来而不是简简单单扔一个字符串字段进交易流水表。1.2 为什么自己写而不是用现成记账软件市面上不是没有好用的记账工具有的 UI 确实设计得不错图表也好看。但用久了你会发现几个绕不开的痛点免费版的功能边界非常模糊很多关键的分析维度、数据导出、多账本能力都锁在付费墙后面。数据全部存放在别人的服务器上导入导出格式不通用一旦平台停止运营你几年的流水全打水漂了。想定制一个属于自己的分类体系或者增加一个字段完全做不到。自己写一套系统所有数据、逻辑、界面都在自己手里。虽然前期要花时间搭框架但后续的扩展非常舒服。比如我想给某笔支出打个标签加一个字段就行没人能拦我。这套系统对我来说更像一个专属工具箱而不是一个通用货架上的商品。1.3 这套系统的适用人群如果你属于下面这几类人这套项目对你会有实际参考价值第一正在学 Python 后端但一直写接口练习没有一个完整项目把登录、增删改查、报表统计串起来的人。这个项目就是一个标准的多模块全栈项目。第二开始接触 Vue想找一个真实的、不是 todo list 级别的项目来练手的人。前端里有表单校验、表格分页、路由权限、图表渲染这些真实业务场景。第三需要交付课程设计或毕业设计的同学源码 数据库 文档三件套可以直接在此基础上做二次开发省去从零搭建的痛苦。第四跟我一样对记账工具有个性化需求的人。代码在手想加功能就加功能想改逻辑就改逻辑。2. 技术选型Python Vue 这套组合的取舍逻辑2.1 后端框架选择Flask 还是 Django这个项目发布后很多人问我的第一个问题都是为什么后端用 Flask 不用 Django我的答案很简单项目业务复杂度决定的。个人财务管理系统的核心业务是账号认证、收支流水管理、分类维护和月度汇总这些用 Flask 加 SQLAlchemy 足以优雅地撑起来。Flask 的路由、请求上下文、蓝图机制对小型项目来说非常清晰调试和后期的逻辑梳理都很顺手。Django 当然也很强自带 Admin、ORM、认证体系但它的重量级对于这种场景来说有点大炮打蚊子而且新手容易迷失在“框架帮你做了太多事”的迷雾里反而搞不懂背后到底发生了什么。我并不是说 Flask 一定比 Django 好不同项目有不同最佳解。但在这个项目里Flask 的轻量就是我选择它的核心理由我可以用最少的框架代码把注意力集中在业务逻辑本身。2.2 前端框架选择Vue 3 Vite 为什么是现在的默认答案前端我选了 Vue并且直接上了 Vue 3。如果你还在纠结到底用 Vue 2 还是 Vue 3我的建议非常明确新项目一律 Vue 3。Vue 3 的 Composition API 在做复杂业务逻辑时复用的爽感是 Vue 2 的 Options API 完全给不了的。比如我在好几个组件里都要用到“获取月度账单汇总”的逻辑直接抽一个useSummary()组合函数哪里需要哪里调用可读性和维护性都大幅提升。构建工具上我用了 Vite 而不是 Webpack。第一次用 Vite 开发时那个冷启动速度真的让人感动代码保存几秒后页面就刷出来了。对于前后端分离的开发模式这种即时反馈非常关键它直接影响了整个开发节奏。Vue 3 生态里 Element Plus 组件库也已经是完全可用的状态表格、表单、对话框、日期选择器这些在做管理系统时高频用到的组件都能直接拿来用省掉了很多手写样式的重复劳动。2.3 数据库与认证方案SQLite/MySQL 与 JWT数据库这块我在开发阶段用了 SQLite部署时可以平滑切换到 MySQL。为什么这么设计因为 SQLAlchemy 这个 ORM 层把数据库差异基本屏蔽掉了SQLite 的文件型数据库在开发时没有任何连接成本改完代码直接运行非常适合快速迭代。到了正式部署阶段换成 MySQL 也只需要改一行连接字符串然后执行一遍迁移脚本模型层代码一行都不用动。认证方案用了 JWT。为什么不用传统的 Session因为前端是 Vue 独立部署的前后端完全分离JWT 的无状态特性天然适配这种架构。用户登录成功后后端签发一个令牌前端把它存起来每次请求在头部带上后端用一个装饰器就能校验身份。整个过程不用管理 Session 的存储和过期对小型项目的实现成本非常低。当然 JWT 也有它的注意事项后续我会专门讲一讲怎么处理令牌过期以及如何在后端做统一的身份识别。3. 数据库设计与后端核心实现3.1 四张核心表的结构设计数据库是整个系统的基础结构设计决定了后面所有功能能不能顺畅实现。我把表拆成了四张用户表users、分类表categories、交易流水表transactions、预算表budgets。用户表users主键 id、用户名 username、密码哈希 password_hash、创建时间 created_at。密码绝对不能存明文我用了 werkzeug 的密码哈希函数来做加盐哈希登录时再校验哈希值。分类表categories主键 id、用户 id、分类名称 name、分类类型 typeincome 或 expense、图标 icon、创建时间 created_at。这里的关键点是分类要归属到用户实现多用户之间的数据隔离。交易流水表transactions主键 id、用户 id、分类 id、金额 amount、类型 type、消费/收入、备注 note、交易日期 transaction_date、创建时间 created_at。amount 我用 Numeric 类型存小数不用 float避免浮点精度问题导致金额对不上。外键关联到 categories 表。预算表budgets主键 id、用户 id、分类 id、月份 month、限额 limit_amount、创建时间 created_at。这里实现的功能是“本月该分类最多能花多少”超了就给你预警提示。你可能会问为什么不把所有信息全塞进一张大表做的时候多省事确实省事但查询统计的时候你会后悔。比如要算“这个月每个分类的花费占比”如果是一张大表每次都得 group by 一个字符串字段而且分类改了名字历史数据全部跟着乱。分表 外键关联初期看着多写了几个模型后面统计和扩展时省的时间远超编写成本。3.2 RESTful API 设计与统一响应格式接口设计我遵循资源导向的 RESTful 风格所有接口围绕资源名词展开配上 HTTP 方法表达操作意图。核心路由如下方法路径功能POST/api/auth/register用户注册POST/api/auth/login用户登录返回 JWTGET/api/transactions获取交易流水列表支持分页和筛选POST/api/transactions新增一笔收支记录PUT/api/transactions/修改指定交易流水DELETE/api/transactions/删除指定交易流水GET/api/summary/monthly获取月度收支汇总GET/POST/api/categories获取/新增分类GET/POST/api/budgets获取/新增预算所有接口返回统一格式的 JSON 结构{ code: 0, message: ok, data: ... }成功时 code 为 0data 放真实数据失败时 code 为非 0message 给出错误原因。这看起来是个小细节但实际体验差别非常大。前端所有请求都走同一个拦截器根据 code 值决定是弹出错误提示还是直接放行数据代码瞬间就简洁了。3.3 后端关键代码认证、流水、统计后端我按蓝图组织代码核心代码可以拆成三段分别讲。第一段是 JWT 认证装饰器的使用。登录成功后签发 access token前端每次请求在 Authorization 头里带上。后端受保护的接口统一加上jwt_required()装饰器然后通过get_jwt_identity()拿到当前登录用户的 ID。所有查询都强制带上这个 user_id天然实现数据隔离用户 A 永远看不到用户 B 的账单。from flask import jsonify, request from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity app.post(/api/auth/login) def login(): data request.get_json() user User.query.filter_by(usernamedata[username]).first() # 校验密码哈希成功后签发令牌 if user and user.check_password(data[password]): token create_access_token(identitystr(user.id)) return jsonify(code0, data{token: token}) return jsonify(code1, message用户名或密码错误)第二段是流水的增删改查。新增一条交易记录时接口会接收前端传来的 category_id、amount、type、transaction_date、note 等字段然后从 JWT 里取当前用户 ID 一起写入数据库。列表查询支持按月份和分类筛选配合 SQLAlchemy 的 filter 条件轻松搞定。第三段是月度汇总统计。这个接口是仪表盘的数据来源我一次返回总收入、总支出、结余以及按分类聚合的支出明细。统计用到 SQLAlchemy 的func.sum和func.group_by一条查询就能把月度数据拉出来。from sqlalchemy import func app.get(/api/summary/monthly) jwt_required() def monthly_summary(): user_id get_jwt_identity() month request.args.get(month) # 格式 YYYY-MM # 按分类聚合当月支出 rows db.session.query( Category.name, func.sum(Transaction.amount).label(total) ).join(Transaction, Transaction.category_id Category.id)\ .filter(Transaction.user_id user_id, Transaction.transaction_date.like(f{month}%), Transaction.type expense)\ .group_by(Category.name).all() return jsonify(code0, data[{name: r[0], total: float(r[1])} for r in rows])这段代码就是系统核心统计能力的缩影。能看出为什么我刚才强调要把分类单独建表因为如果分类只是一个字符串字段这里就很难做到干净的 join 聚合。4. 前端 Vue 实战从初始化到页面落地4.1 环境搭建与项目初始化要点前端初始化对新手来说可能会遇到一堆问题。我这个项目用的是 Vue 3 Vite命令很简单npm create vitelatest finance-web -- --template vue cd finance-web npm install然后装上 Vue Router、Pinia、Axios、Element Plus 这几个关键依赖。npm install vue-router4 pinia axios element-plus重点提醒安装依赖这一步一定要确认网络环境稳定npm install中途断掉或目录报错是新手最常见的事故现场。如果一直卡得很厉害可以检查一下 npm 镜像源是不是默认的国外源换成国内镜像源能解决大部分安装慢的问题。接下来是开发代理配置。前后端分离开发时前端端口默认是 5173后端 Flask 是 5000直接在前端页面里请求http://localhost:5000/api/...一定会遇到跨域问题。我最推荐的做法是在 Vite 的配置里加代理把/api开头的请求转发到后端地址开发时前端代码里写相对路径部署时几乎不用改代码。// vite.config.js export default defineConfig({ plugins: [vue()], server: { proxy: { /api: { target: http://localhost:5000, changeOrigin: true } } } })这里有个非常关键的细节代理解决的是前端开发服务器的转发问题但如果后端接口没有开启 CORS生产环境部署后跨域问题一样会冒出来。所以后端也要配一下 Flask-CORS两层保险都做上。4.2 axios 封装与接口调用所有请求我统一封装在一个request.js模块里。拦截器里做几件事给每个请求头注入 JWT 令牌响应回来后统一取出data字段遇到 401 状态码时跳回登录页网络异常时弹出一个统一的提示框。这样每个页面组件里写业务代码时只需要关心拿到数据做什么不用把错误处理逻辑重复几十遍。import axios from axios const service axios.create({ baseURL: /api, timeout: 8000 }) service.interceptors.request.use(config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) service.interceptors.response.use( res { if (res.data.code 0) { return res.data.data } ElMessage.error(res.data.message || 请求失败) return Promise.reject(new Error(res.data.message)) }, err { if (err.response err.response.status 401) { router.push(/login) } ElMessage.error(网络异常请稍后重试) return Promise.reject(err) } )这么一封后面的接口调用就非常清爽了。比如仪表盘页面拉取月度数据核心代码就是一行const data await getMonthlySummary(month)数据直接就到了前端手里剩下的都是渲染逻辑。4.3 仪表盘与账单管理页面实现仪表盘页面是系统的门面我放了四个卡片本月收入、本月支出、本月结余、预算提醒。下面接一个 ECharts 的环形图展示本月各分类支出占比。ECharts 配合 Vue 3 用起来很方便图表配置项通过响应式数据驱动数据变化时图表自动更新。账单管理页面是操作最频繁的模块。顶部是一个搜索栏支持选择月份、选择分类、关键词搜索。中间是 Element Plus 的表格展示日期、分类、金额、备注、编辑按钮和删除按钮。底部分页组件配合后端的分页接口。新增和编辑共用同一个弹窗表单表单里做必要的校验比如金额必须大于 0 且不能为空。实际写这个页面的过程中我最喜欢 Composition API 的一点就是把搜索条件定义成响应式变量后后端查询条件的变化直接绑定到 URL 参数上一刷新页面搜索条件还在这个体验在 Vue 2 里实现起来要绕不少弯子。5. 全套源码、数据库与文档的组织方式5.1 项目目录结构怎么规划这套系统交付时包含源码、数据库和文档三部分目录结构从一开始就要理清楚不然最后整理的时候会非常痛苦。我采用前后端分离的目录布局根目录下分成backend、frontend、docs三个文件夹每个文件夹内部又有自己清晰的层级。backend/app.pyFlask 应用入口models.pySQLAlchemy 模型定义routes/按模块拆分的蓝图路由requirements.txtPython 依赖清单finance.dbSQLite 数据库文件frontend/src/api/axios 封装和接口请求函数src/views/页面组件src/components/公共组件src/store/Pinia 状态src/router/路由配置docs/API文档.md接口文档部署文档.md环境配置和部署说明数据库说明.md表结构和初始化脚本说明这个结构的意义在于任何一个有经验的开发者拿到这个项目不用问任何人五分钟之内就能定位到要改的文件在哪。好的项目结构本身就是最好的注释。5.2 配套文档要写什么很多人写文档喜欢贴大段代码看起来厚厚一本实际上价值很低。我的文档思路是把读文档的人想象成一个刚拿到项目的新人他要能从零开始把这个系统跑起来然后知道每个模块在干什么。部署文档里必写的是环境版本要求Python 3.9、Node 16、npm 版本然后是两条主线一是后端怎么创建虚拟环境、装依赖、启动二是前端怎么装依赖、启动、配置代理。数据库说明里要把四张表的字段、类型、外键关系写清楚顺便给一份初始分类数据的 SQL 脚本这样新人不用手动去敲一堆分类词条。API 文档是前后端对接的依据。我强烈建议按资源逐个写清路径、请求方法、请求参数、返回示例。比如交易流水的列表接口参数里有 month、category_id、page、page_size返回结构是什么样写清楚了前端直接照着用联调效率高得不是一点半点。5.3 部署上线的两种路径最后的部署路径我给两套方案。第一套是开发级部署适合自己玩玩或者演示用后端跑在服务器上Flask 用默认的开发服务器监听 5000前端npm run build之后把 dist 目录里的静态文件丢给 NginxNginx 同时做反向代理把/api的请求转发到后端。这样一台服务器就能搞定整个系统。第二套是正式级部署适合长期使用后端用 Gunicorn 起多个 worker 进程并用 systemd 做成守护进程数据库从 SQLite 换成 MySQL前端静态文件扔到 CDN接口统一走 HTTPS。这套方案稳定性和性能都明显更好但配置工作也翻倍。我自己的使用场景属于第一套就够的类型。个人记账系统的并发量本来就很低SQLite 加 Flask 开发服务器已经能跑得非常稳完全没必要一开始就上很强的架构。6. 实操中踩过的坑与排查手册6.1 跨域问题不是玄学是你哪里没配好跨域报错基本上是前后端分离项目里出现频率最高的问题。我调试这个系统时一开始只在后端装了 Flask-CORS本地开发时看着没问题结果前端同事我用另一台电脑模拟的一打开页面就报 CORS 错误。排查到最后才发现Vite 代理只解决了本地开发机的场景部署后前端静态资源所在的域名跟后端接口域名不一样跨域就得靠后端响应头来解决。所以最终的方案是双保险开发时用 Vite proxy减少本地联调的麻烦生产环境后端必须配上 CORS 白名单把前端域名明确加进去。这两个都做了跨域基本就不会再找你了。6.2 中文乱码与时间时区陷阱数据库里中文乱码这个问题SQLite 基本不会遇到但如果你换成 MySQL创建表的时候没有指定 utf8mb4 字符集插入中文就很容易变成问号。解决方案是在数据库连接字符串里明确字符集参数并且建表时统一ENGINEInnoDB DEFAULT CHARSETutf8mb4。另一个不容易察觉的坑是时区问题。Python 的datetime.now()拿的是服务器本地时间如果以后部署到云主机而云主机的时间是 UTC记账时间就会跟实际差八小时。我的做法是统一用 UTC 存数据库展示时再转成北京时间的格式这样不管服务器在哪个地区数据本身永远是对的展示层按需转换就行。6.3 依赖版本不匹配的经典报错开发这套系统时我踩过最莫名其妙的一个坑就是npm install装了一大堆依赖之后启动项目提示 peer dependency 冲突。原因是 Element Plus 和某个版本的 Vue 插件兼容性出了问题。后来的解决方案是重新生成了一遍锁文件用 npm 的overrides字段把冲突的依赖版本固定下来。这个问题的教训是装依赖的时候不要一股脑全执行先装核心的再逐个加出了问题能知道是哪个包引起的。后端这边也有一个相似的问题。Flask-SQLAlchemy 和 SQLAlchemy 的新版本有兼容性警告如果直接装最新版跑起来会看到一大串 deprecation 提示。这个对功能没有实际影响但排查问题时会干扰视线。解决方法是把requirements.txt里的版本号精确固定到我测试通过的版本而不是用这种宽松写法。6.4 排查问题的方法论最后分享一个通用的排查思路遇到问题别慌按这个顺序来第一步看浏览器开发者工具里的 Network 面板确认请求发出去了没有、状态码是什么、响应体里报错信息是什么第二步看后端控制台的日志输出有没有抛异常、异常在哪个文件哪一行第三步确认数据库状态看表存不存在、字段名对不对、数据有没有成功写入。绝大部分问题都出在这三层中的某一层。把问题定位到具体层之后去搜报错信息时也更有针对性不会在茫茫帖子里大海捞针。我在排查这个系统的问题时至少有两次是后端明明写对了但前端调接口时路径少了个斜杠一直在浏览器里转圈最后核对 Network 面板才发现请求根本没发出去。写在最后这套个人财务管理系统做完之后我自己一直在用。开发的时候以为记账这个需求很简单真正把数据库表拆开、把统计接口一个个写出来之后才发现一个好的记账工具真正考验的是数据结构的设计和聚合统计的思考代码本身反而是最简单的一环。我印象最深的是第一次把月度分类统计的环形图跑出来那天晚上看到自己的开支以可视化的方式呈现那种“原来钱都花在这了”的冲击力确实很直观。这个系统的每一次迭代都解决了我真实使用中的一些小痛点比如后来加的预算提醒功能每个月餐饮超支时仪表盘上的红色预警都特别扎眼确实帮我克制了不少冲动消费。这套源码和文档目前已经整理成了可以直接落地的状态。如果你正在学 Python 和 Vue想找一个完整项目把前后端的知识点串起来拿它当参考骨架是最省力的路径。拿到手之后先跑通再按你自己的记账习惯去改分类、改页面它才能真正变成你的工具。