Django 家庭财务管理系统:从数据模型到报表统计的完整实现

发布时间:2026/9/14 6:28:57
Django 家庭财务管理系统:从数据模型到报表统计的完整实现 简介这是一套基于Python与Django框架开发的家庭财务管理系统毕业设计源码包面向计算机相关专业学生的毕设、课设或课程实践。系统包含前台用户注册登录、收支信息登记与查询修改、个人资料管理以及后台成员管理、收支管理、新闻公告发布等功能覆盖家庭财务常见业务闭环。包体内含完整源代码、数据库脚本与文档说明共2000个文件其中以1626个JS脚本、256个HTML页面、52个CSS样式文件为主另有少量Python源码、数据库结构及说明文档压缩包仅5.51MB结构紧凑便于部署和学习。目前已有228人学习下载。借助这套资料可以快速理解Django MTV架构、实体关系设计与基础增删改查实现也能直接作为毕业设计演示项目使用。所有代码均在PyCharm Django2.2 Python3.6 MySQL5.6环境下测试运行成功并附带README说明适合二次开发或功能扩展。1. 为什么家庭财务管理系统值得用 Django 完整做一遍年底对着 Excel 里的流水对不上账是很多家庭和独立开发者都经历过的场景。用电子表格记账的最大问题不是记录本身而是分类口径不统一同一笔“美团外卖”这个月记在“餐饮”下个月记在“生活日用”到年底统计时数据就失真了。与其反复在表格里修公式不如用 Django 把“记账”这件事做成一个带明确数据模型、可查询、可统计的小系统。这个标题里的“源代码 文档说明 数据库”三个要素恰好对应 Django 项目的三件套应用代码、README 与迁移文件、SQLite 或 MySQL 的落地数据。对正在做课程设计、软件综合实践选题或者想给家里搭一个长期记账工具的开发者来说这是一条完整且可复现的路径。Django 的 ORM、Admin 后台和模板系统能让 90% 的增删改查逻辑不重写把精力集中在分类统计和报表展示上。2. 数据模型与项目骨架先把流水表和分类表定稳家庭财务系统的核心不是页面而是三张表账户表、分类表、流水表。账户表管钱在哪现金、微信、银行卡分类表管钱花在哪餐饮、交通、医疗流水表记录每一笔收入和支出的明细。这三张表的关系定清楚了后面的统计、报表、导出都不会跑偏。2.1 用 Django ORM 还是直接写 SQL选型理由这个项目标题里同时出现了“Python”“Django”“数据库”说明目标是一个可交付的完整项目而不是一个跑通就丢的脚本。直接用 SQLite 配合原生 SQL 当然能实现但家庭财务系统的查询模式很固定——按时间范围查流水、按分类聚合金额、按月统计结余——这些用 Django ORM 的filter()、annotate()、values()组合写比拼 SQL 字符串更安全也更容易在后期加权限控制。还有一个实际原因课程设计和综合实践类选题通常要求展示“工程化能力”Django 自带的迁移文件、Admin 后台和模板引擎本身就是可展示的产出物。用 Flask 也能做但 Flask 需要自己组装 SQLAlchemy、表单验证和 Admin 组件对一个以“完整交付”为目标的项目来说Django 的一体化设计更省事。2.2 三张核心表的字段设计家庭财务系统的模型设计要贴近真实记账习惯。账户表不需要太复杂name和balance就够了余额可以在流水录入时同步更新也可以后期用聚合函数重算。分类表一定要区分“收入类”和“支出类”否则统计收支时会混在一起。流水表是重头戏至少要有金额、类型、分类外键、账户外键、交易日期、备注六个字段。# finance/models.py from django.db import models from django.utils import timezone class Account(models.Model): 账户表现金、微信、银行卡等资金载体 name models.CharField(账户名, max_length50, uniqueTrue) balance models.DecimalField(当前余额, max_digits12, decimal_places2, default0) created_at models.DateTimeField(auto_now_addTrue) def __str__(self): return self.name class Category(models.Model): 分类表餐饮、交通、工资等用 type 区分收入/支出 TYPE_CHOICES ( (income, 收入), (expense, 支出), ) name models.CharField(分类名, max_length30) type models.CharField(类型, max_length10, choicesTYPE_CHOICES) parent models.ForeignKey(self, nullTrue, blankTrue, on_deletemodels.CASCADE, related_namechildren, verbose_name父分类) class Meta: unique_together (name, type) verbose_name 分类 verbose_name_plural 分类 def __str__(self): return f{self.get_type_display()}: {self.name} class Transaction(models.Model): 流水表每一笔收支记录 TYPE_CHOICES ( (income, 收入), (expense, 支出), ) amount models.DecimalField(金额, max_digits12, decimal_places2) type models.CharField(类型, max_length10, choicesTYPE_CHOICES) category models.ForeignKey(Category, on_deletemodels.PROTECT, verbose_name分类) account models.ForeignKey(Account, on_deletemodels.PROTECT, verbose_name账户) date models.DateField(交易日期, defaulttimezone.now) note models.CharField(备注, max_length200, blankTrue) created_at models.DateTimeField(auto_now_addTrue) class Meta: ordering [-date, -created_at] indexes [ models.Index(fields[date]), models.Index(fields[category, date]), ] def __str__(self): return f{self.date} {self.get_type_display()} {self.amount}代码里有三个细节值得注意。on_deletemodels.PROTECT表示如果分类或账户已经被流水引用就不能直接删除——这个设计比默认的CASCADE更符合财务系统的安全要求避免误删分类导致历史账单格式错乱。DecimalField而不是FloatField存金额是因为浮点数在 Python 里做加减时会出精度问题比如0.1 0.2不等于0.3而DecimalField在数据库层面就保证了精度。unique_together约束让同一个类型下不能出现两个同名的分类这对后续下拉框渲染很有用。2.3 创建 app 与 settings 配置要点创建 Django 项目时我一般会按功能拆 app而不是把全部模型堆在一个models.py里。这个项目规模不大拆两个 app 足够accounts管用户认证finance管账务数据。# 项目初始化命令 django-admin startproject family_finance . python manage.py startapp finance python manage.py startapp accounts # 数据库迁移 python manage.py makemigrations finance python manage.py migrate python manage.py createsuperusersettings.py里需要配置INSTALLED_APPS把finance和accounts注册进去。LANGUAGE_CODE改成zh-hansTIME_ZONE改成Asia/Shanghai这样 Django Admin 后台会显示中文日期时间也会按中国时区处理。如果后续要连 MySQL 而不是默认的 SQLite把DATABASES字典里的ENGINE换成django.db.backends.mysql加上NAME、USER、PASSWORD、HOST、PORT五件套然后再安装mysqlclient驱动。数据库同步工具方面Django 自带的migrate命令就是最稳的同步方式不要手动去改表结构。3. 记账核心功能从 ModelForm 到事务处理模型定好之后下一步是把“记一笔”这个操作做成表单、校验、入库的完整链路。家庭财务系统的记账功能看似简单但如果不在这个环节处理好分类联动、金额校验和账户余额更新后面的统计数据一定会有脏数据。3.1 用 ModelForm 减少重复代码Django 的ModelForm是这套系统里性价比最高的组件。只需要声明模型和字段列表表单渲染、字段验证、错误提示就都出来了。对于家庭财务这种字段较少、规则明确的场景完全不需要手写 HTML 表单。# finance/forms.py from django import forms from .models import Transaction class TransactionForm(forms.ModelForm): class Meta: model Transaction fields [amount, type, category, account, date, note] widgets { date: forms.DateInput(attrs{type: date}), note: forms.TextInput(attrs{placeholder: 选填例如买菜、加油}), } def clean(self): 跨字段校验金额必须大于 0类型和分类必须匹配 cleaned_data super().clean() amount cleaned_data.get(amount) type cleaned_data.get(type) category cleaned_data.get(category) if amount is not None and amount 0: self.add_error(amount, 金额必须大于 0) if category and type: # 分类的 type 必须和流水的 type 一致 if category.type ! type: self.add_error(category, 分类类型与收支类型不匹配) return cleaned_dataclean()方法是表单校验的核心钩子。在这个项目里最常见的误操作就是在“支出”流水里选了“工资”这个收入分类如果不在表单层拦截后期分类统计时“支出”总和会被这个脏数据污染。widgets里把日期字段指定为typedate的原生日期选择器浏览器会直接渲染出日历控件比手动输入2025-01-11这种格式友好得多。3.2 视图函数里的创建与更新操作表单通过校验后视图函数的逻辑就变得非常直接。创建和更新流程都围绕form.save()展开但有一个关键点要在保存流水的同时更新对应账户的余额。# finance/views.py from django.shortcuts import render, redirect, get_object_or_404 from django.contrib import messages from django.db import transaction as db_transaction from .models import Transaction, Account from .forms import TransactionForm def transaction_create(request): if request.method POST: form TransactionForm(request.POST) if form.is_valid(): with db_transaction.atomic(): # 先保存流水再更新账户余额 txn form.save(commitFalse) txn.save() account txn.account if txn.type income: account.balance txn.amount else: account.balance - txn.amount account.save() messages.success(request, 记账成功) return redirect(finance:transaction_list) else: form TransactionForm() return render(request, finance/transaction_form.html, {form: form})这段代码里的db_transaction.atomic()是重点。如果没有事务包裹流水保存成功后、账户余额更新前如果发生异常就会出现流水存在但余额没变的脏状态。commitFalse先拿到未入库的对象等余额更新逻辑都准备好后再save()。实际操作中我一般会把“更新账户余额”封装成Transaction模型里的一个方法比如txn.apply_to_account()这样创建和编辑操作都能复用同一套逻辑。3.3 删除操作的三种常见坑删除流水比创建更容易出错。如果允许用户随意删除流水而不同步更新账户余额数据库里账户的balance字段就会逐渐失真。三种常见做法是直接删除余额不动、删除并反向更新余额推荐、软删除加is_deleted字段保留历史。推荐的做法是删除时反向更新余额——收入流水删除时余额减掉对应金额支出流水删除时余额加上对应金额。# finance/views.py def transaction_delete(request, pk): txn get_object_or_404(Transaction, pkpk) if request.method POST: with db_transaction.atomic(): account txn.account if txn.type income: account.balance - txn.amount else: account.balance txn.amount account.save() txn.delete() messages.success(request, 流水已删除) return redirect(finance:transaction_list)视图里通过request.method POST来区分删除确认这是 Django 处理删除操作的标准做法。直接用 GET 请求删除容易被 CSRF 攻击或误触发所以删除按钮在模板里要包在一个form里用 POST 提交。django.contrib.messages用来在页面顶部显示操作结果比直接在页面里写死提示语灵活得多。操作场景是否更新账户余额事务要求创建收入流水余额增加必须创建支出流水余额减少必须删除收入流水余额减少必须删除支出流水余额增加必须编辑流水金额先反向再正向必须编辑操作的逻辑是删除和创建的组合先把旧流水的金额反向更新到账户余额再用新金额正向更新。如果不做这两步账户余额会按差额累计误差。4. 月度报表与分类统计用聚合查询算清每一笔钱记账只是手段看明白钱花在哪才是目的。Django ORM 内置的聚合函数Sum、Count、Avg配合annotate()和values()能在一个查询里完成按月、按分类的统计不需要写复杂的 SQL。4.1 用 annotate 和 values 做月度汇总“这个月一共花了多少钱”是最常见的查询需求。实现思路是先把流水按月份字符串分组再用Sum聚合支出金额。# finance/views.py from django.db.models.functions import TruncMonth from django.db.models import Sum, Count def monthly_report(request): 按月统计支出返回 [{month: 2025-01, total: 1234.56}, ...] monthly_data ( Transaction.objects .filter(typeexpense) .annotate(monthTruncMonth(date)) .values(month) .annotate( totalSum(amount), countCount(id) ) .order_by(-month) ) return render(request, finance/monthly_report.html, {monthly_data: monthly_data})TruncMonth是 Django 3.2 之后提供的数据库函数会把日期字段截断到月份精度比如2025-01-11和2025-01-28会归到同一个2025-01-01分组。values(month)告诉 ORM 按这个字段分组后面的annotate里再用Sum和Count算出每个月的总支出和笔数。这个查询翻译成 SQL 就是SELECT DATE_TRUNC(month, date) AS month, SUM(amount) FROM finance_transaction WHERE typeexpense GROUP BY month但用 ORM 写不用关心不同数据库的日期函数差异。4.2 分类占比单一查询统计多类目要计算“餐饮占总支出的 40%”需要一次性取出所有分类的支出总和再在 Python 层计算占比。values(category__name)会跨表关联 Category 模型取出分类名称作为分组依据。# finance/views.py def category_report(request, yearNone, monthNone): 按分类统计支出金额与占比 qs Transaction.objects.filter(typeexpense) if year and month: qs qs.filter(date__yearyear, date__monthmonth) category_data ( qs.values(category__name) .annotate(totalSum(amount)) .order_by(-total) ) grand_total sum(item[total] for item in category_data) for item in category_data: item[percent] round(item[total] / grand_total * 100, 1) if grand_total else 0 return render(request, finance/category_report.html, { category_data: category_data, grand_total: grand_total, })这里有一个常见的性能误区如果流水表有几万条在 Python 层用sum()计算总支出是可以接受的但如果再做更复杂的多表关联统计就应该用django.db.models.Sum直接在数据库层算完只把结果集传到模板。values(category__name)用双下划线做跨表查询的字段路径这是 Django ORM 最核心的语法之一熟练掌握filter、values、annotate三个函数的组合能覆盖绝大多数报表需求。4.3 模板渲染与图表展示统计结果最终要展示成图表才直观。模板里可以用Chart.js的 CDN 引入折线图和饼图把 Django 传递的列表数据转成 JSON 传给前端。!-- templates/finance/category_report.html -- table classtable thead trth分类/thth金额/thth占比/th/tr /thead tbody {% for item in category_data %} tr td{{ item.category__name }}/td td¥{{ item.total }}/td td{{ item.percent }}%/td /tr {% endfor %} /tbody /table canvas idcategoryPieChart width400 height200/canvas script const ctx document.getElementById(categoryPieChart); const labels {{ category_data|length }}; const data {{ category_data|length }}; // 用 JSON 序列化标签和数据Chart.js 才能识别 new Chart(ctx, { type: pie, data: { labels: [餐饮, 交通, 购物], datasets: [{ data: [300, 200, 100], backgroundColor: [#ff6384, #36a2eb, #cc65ff] }] } }); /script模板里的{{ category_data|length }}这种写法只适合测试实际项目中需要在视图里用json.dumps()把数据转换成 JSON 字符串再用mark_safe()标记为安全内容传给模板。不这样做的话模板中的中文分类名会被转义成 Unicode 编码图表显示会乱码。4.4 查询优化select_related 与索引当流水表数据量上来之后列表页会明显变慢。Transaction.objects.all()默认不会自动关联查询Account和Category表而是在模板渲染txn.account.name时逐条发起查询这就是经典的 N1 问题。解决办法是用select_related()在查询时一次性把外键对象取出来。# finance/views.py def transaction_list(request): transactions ( Transaction.objects .select_related(account, category) .all() ) return render(request, finance/transaction_list.html, {transactions: transactions})select_related生成的 SQL 是LEFT OUTER JOIN把account和category两张表的数据一起查出来。字段索引也很重要如果经常按date范围查询就给date字段建索引如果经常按category聚合就给category和date建联合索引。模型定义里第二个indexes选项就是干这个的。数据量超过十万条时索引带来的性能提升远比代码层面优化明显。5. 数据库迁移与项目文档的落地习惯系统功能做完剩下的工作同样重要把开发环境的 SQLite 数据切换成生产可用的 MySQL把项目结构、运行步骤和数据库说明写清楚。这个标题里面明确写了“数据库.zip”和“文档说明”说明交付物里这两项是硬性要求不能只是把代码打包就完事。5.1 从 SQLite 切换到 MySQL 的完整流程SQLite 适合开发调试但课程设计、期末项目或正式部署时评委和用户大概率会要求 MySQL。切换过程分三步走安装驱动、改配置、迁移数据。# 安装 mysqlclient 驱动 pip install mysqlclient # 如果 pip 安装失败Windows 常见改用二进制包安装 # 或安装 PyMySQL 后在 settings.py 里指定settings.py里的DATABASES配置改成DATABASES { default: { ENGINE: django.db.backends.mysql, NAME: family_finance, USER: root, PASSWORD: your_password, HOST: 127.0.0.1, PORT: 3306, } }改完配置后需要在 MySQL 里手动创建数据库注意字符集要指定为utf8mb4否则中文备注和分类名可能报Incorrect string value错误。CREATE DATABASE family_finance CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;然后重新跑python manage.py migrateDjango 会自动把models.py里的模型定义转换成 MySQL 的表结构。数据迁移方面Django 没有内置的跨数据库复制功能我一般用dumpdata导出 JSON再切换数据库后用loaddata导入。# 在 SQLite 环境导出数据 python manage.py dumpdata --exclude auth.permission --exclude contenttypes data.json # 切换数据库后导入 python manage.py loaddata data.json注意loaddata时要先导入auth相关的用户数据再导入业务数据否则外键关联可能会失败。如果数据量很大用这条命令效率会比较低但家庭财务系统的数据量通常都在几千条以内这个方案已经足够。5.2 文档说明应该包含哪些内容“文档说明”不是指代码注释而是交付物里单独的一份 README 或说明文档。我见过太多课程设计项目代码里写满了注释README 却只有两行安装命令这对使用者来说是灾难。一份合格的文档至少要有项目简介、技术栈、运行步骤、默认账号、目录结构说明、数据库表关系说明。文档章节必写内容项目简介系统能做什么、面向谁、核心功能列表技术栈Python 版本、Django 版本、数据库类型运行步骤克隆代码 → 创建虚拟环境 → 安装依赖 → 迁移 → 启动默认账号管理员用户名/密码、测试用户软件综合实践类的项目文档里最好再加一段“功能测试说明”写清楚哪些核心功能可以手动验证——比如新增一笔流水后账户余额是否正确变化、删除后是否反向更新。这比写一万字设计理念更有价值。5.3 打包交付时的目录组织最后一步是把项目按照可交付的标准整理目录。只需要提交requirements.txt、README.md、源代码目录、数据库导出文件不要把虚拟环境、__pycache__、db.sqlite3这些运行产物打进去。# 导出依赖清单 pip freeze requirements.txt # 清理运行产物 rm -rf __pycache__/ */__pycache__/ db.sqlite3 media/如果数据库是 MySQL单独导出一份 SQL 文件放在database/目录下并在 README 里写明导入方法。如果是 SQLite直接把db.sqlite3打进去也可以但要注明这是开发数据正式使用前应清空。用一个.gitignore把不需要的文件排除掉这份交付物就能同时满足代码评审、数据验收和二次开发三种场景。5.4 Django Admin 界面美化的最少改动这个系统里 Admin 后台是管理分类和账户的最快入口。Django 自带的 Admin 界面比较朴素但通过list_display、list_filter、search_fields三个属性不需要写任何前端代码就能让后台变得好用。在finance/admin.py里注册模型时加上这些配置# finance/admin.py from django.contrib import admin from .models import Account, Category, Transaction admin.register(Transaction) class TransactionAdmin(admin.ModelAdmin): list_display [date, type, category, amount, account, note] list_filter [type, category, account, date] search_fields [note, category__name] date_hierarchy date list_per_page 50date_hierarchy会在后台列表页顶部生成一个按年份、月份逐级筛选的日期导航栏这是查看历史账单最高效的方式。search_fields里的category__name表示可以按分类名称搜索流水这是 Django Admin 支持跨表搜索的语法。本文还有配套的精品资源点击获取