
简介这是一套基于Django框架与Python语言实现的家庭财务管理系统适用于毕业设计、课程设计及Web开发入门练习。它针对家庭用户记账繁琐、预算失控等痛点提供账户管理、收支记录、预算控制、报表统计四大核心模块支持用户注册登录、收支分类筛选、预算追踪与可视化分析并整合MySQL数据库与多重安全机制形成可运行的完整Web应用。压缩包共包含2000个文件整体约4.52MB其中以1626个JavaScript脚本、257个HTML页面、52个CSS样式表及19个JSON文件为主另有少量Python源码与配置文档前端页面与后端逻辑分离清晰导入环境即可查看效果方便按模块检索学习。资源不仅覆盖系统全部源码与页面还体现出Django的MTV架构与数据库交互方式包含业务逻辑分层与常用配置可支撑答辩演示、功能仿写或二次开发。目前已有41人学习浏览适合需要快速搭建同类型管理系统的学习者参考借鉴。1. 普通记账到家庭账本这个 Django 项目到底能落地哪些东西打开这类《基于 Django 的家庭财务管理系统》压缩包之前最好先有一个预期它不只是一堆能跑的页面而是一个把「记账、分类、预算、统计、用户权限」串起来的最小闭环。我拆过几份同名的包结构参差不齐但最核心的骨架高度一致——一个 Web 框架下的多 App 协作外加权限隔离和数据导出。对 Django 项目实战新手来说这份资源最有价值的地方不是界面多好看而是能看清「一个完整系统怎么拆模块、怎么处理表和表之间的关系、权限在哪一层做才不翻车」。它在家庭场景下解决了三件事零散账目集中管理、家庭成员各自记账互不干扰、月底能拿数据说话而不是靠回忆。适合两类人一类是刚把 Django 教程刷完、想完整走一遍项目全流程的初学者另一类是课程设计要求交付一个可运行系统的学生。2. 五个 App 和三张核心表先把系统骨架读透2.1 为什么是五个 App而不是一个 views.py 写到底刚接触 Django 的人最容易犯的错是把所有模型往一个 models.py 里塞所有视图堆在一个 views.py 里。这套家庭财务系统的拆法更接近真实工程按业务边界拆 App。我拆过的版本里典型的五个 Dango App 分别是accounts 管理用户和家庭、ledger 管收支流水、category 管分类树、budget 管预算、stats 管统计图表。这个拆分思路本身是教科书级别的记账功能往后加字段不影响用户登录模块预算算出问题去 budget 里查不用在几千行的 ledger views 里面捞。Django 的 App 机制本来就是干这个用的——每个 App 是独立的应用单元有自己的 models、views、urls。创建命令很简单python3 manage.py startapp ledger python3 manage.py startapp category python3 manage.py startapp budget创建完记得在项目 settings.py 的 INSTALLED_APPS 列表里把新 App 加进去这一步漏了migrate 时数据库不会建任何表运行后访问相关路由直接 404。我见过不少人卡在这个「建了 App 但没注册」的经典坑上报错信息里根本没有提示只能对着框架文档一行行查。五个 App 之间不是平级乱调而是有依赖方向ledger 依赖 accounts 拿到用户信息和家庭 ID也依赖 category 拿分类外键stats 只读 ledger 和 budget 的数据做聚合和输出。这种单向依赖很重要如果两个 App 互相 import modelsDjango 初始化时很容易报循环导入错误项目一大就更难排查。2.2 三张核心表Transaction、Category 和 Budget 的字段设计这三张表决定了系统能不能支撑家庭记账的真实需求。先看交易流水表也常叫 Expense / Income / Record不同包命名不同但逻辑一致它的最小字段设计大概是这样的# ledger/models.py from django.db import models from django.contrib.auth.models import User class Transaction(models.Model): TRANSACTION_TYPES ( (expense, 支出), (income, 收入), ) user models.ForeignKey(User, on_deletemodels.CASCADE, verbose_name记账人) family models.ForeignKey(accounts.Family, on_deletemodels.CASCADE, verbose_name所属家庭) category models.ForeignKey(category.Category, on_deletemodels.SET_NULL, nullTrue, blankTrue, verbose_name分类) amount models.DecimalField(max_digits10, decimal_places2, verbose_name金额) transaction_type models.CharField(max_length10, choicesTRANSACTION_TYPES, verbose_name类型) note models.CharField(max_length255, blankTrue, verbose_name备注) occurred_at models.DateTimeField(verbose_name发生时间) created_at models.DateTimeField(auto_now_addTrue, verbose_name创建时间) class Meta: ordering [-occurred_at] indexes [ models.Index(fields[family, occurred_at]), ]这里的逻辑要注意三点。第一on_deletemodels.SET_NULL配合nullTrue意味着分类被删掉后历史流水不会跟着消失这条记录仍然能保留只是分类处显示空。如果写成 CASCADE删一个分类会把该分类下的所有账单全部清掉家庭账本直接没了一半数据这是血泪教训。第二金额用 DecimalField 而不是 FloatField记账系统涉及钱浮点误差在累加统计时会被放大Decimal 才能保证小数精度。第三Meta 里那个索引很关键按家庭和时间范围筛选是最常见的查询场景不加索引数据到几万条之后列表页会明显变慢。再来看分类表。家庭记账场景里分类往往是多级的比如「餐饮」下面还有「早餐」「午餐」「外卖」「交通」下面分「公交」「打车」「油费」。树形结构在 Django 里最朴素的实现是自关联外键# category/models.py class Category(models.Model): name models.CharField(max_length50, verbose_name分类名) parent models.ForeignKey(self, on_deletemodels.CASCADE, nullTrue, blankTrue, verbose_name父分类) family models.ForeignKey(accounts.Family, on_deletemodels.CASCADE, verbose_name所属家庭) sort_order models.IntegerField(default0, verbose_name排序)第三张核心表是预算表。预算功能本质上就是给「分类 时间周期 限额」做绑定# budget/models.py class Budget(models.Model): family models.ForeignKey(accounts.Family, on_deletemodels.CASCADE, verbose_name家庭) category models.ForeignKey(category.Category, on_deletemodels.CASCADE, verbose_name分类) period models.CharField(max_length20, choices((monthly, 月度), (yearly, 年度)), defaultmonthly, verbose_name周期) limit_amount models.DecimalField(max_digits10, decimal_places2, verbose_name预算限额) created_at models.DateTimeField(auto_now_addTrue)预算的实际逻辑在视图层做当月某分类下支出流水求和超过 limit_amount 就在页面给出警告或标记超额。这套设计里比较容易被忽略的是「预算按家庭算还是按个人算」——多数这个体量的项目按家庭汇总来算属于合理取舍不必强行做单人维度。2.3 权限隔离不是每个视图里加 if而是用中间件统一拦家庭财务系统最敏感的诉求是隐私。同一家庭里爸妈能看全家账孩子可能只能看到自己的零花钱记录。这套系统里的用户表和家庭表是分开的User 是 Django 自带的认证模型Family 是独立的账户模块模型两者通过外键或中间表关联。权限控制最常见做法是用 Django 的login_required装饰器配合自定义 mixin 做家庭归属校验。但纯装饰器容易漏——新增一个视图时忘了加装饰器整个接口裸奔。更稳的做法是写一个统一的权限检查中间件# accounts/middleware.py from django.shortcuts import redirect from django.urls import resolve class FamilyAccessMiddleware: def __init__(self, get_response): self.get_response get_response def __call__(self, request): # 放行登录页、静态资源和公开接口 if request.path.startswith(/static/) or request.path in (/login/, /register/): return self.get_response(request) if not request.user.is_authenticated: return redirect(/login/) # 核心检查当前用户是否已绑定家庭 if not hasattr(request.user, profile) or not request.user.profile.family: return redirect(/bind-family/) return self.get_response(request)这个中间件放在 settings.py 的 MIDDLEWARE 列表里放在 SessionMiddleware 之后即可。它的好处是全局生效新增接口默认受保护不用记着在每个视图函数上加装饰器。在这个资源包里如果看到类似写法说明作者是有真实工程经验的如果只看到零散装饰器也没关系自己补一层中间件即可。3. 记账视图与权限细节几十行代码背后的行为差异3.1 登录、注册和家庭绑定入口处把所有用户隔开登录模块基本是 Django 认证体系的标准用法。Django 的authenticate和login组合能解决大部分问题但家庭财务系统的特殊性在于账号登录后必须知道自己属于哪个家庭否则后面所有按 family 过滤的查询都会出错。注册流程的差异点在于多了一个「绑定家庭」的步骤。常见做法有两种第一种是新用户注册时输入一个家庭邀请码系统根据邀请码找到家庭记录并绑定第二种是注册时直接创建一个新家庭用户就是家庭创建者。资源包一般会做第二种简单直接但家庭成员多了以后会面临「一个家庭多个账号怎么合并」的问题。如果要做第一种需要在注册表单里加一个family_code字段校验逻辑放在form.is_valid()里# accounts/forms.py class RegisterForm(forms.ModelForm): family_code forms.CharField(max_length10, requiredFalse) def save(self, commitTrue): user super().save(commitFalse) user.set_password(self.cleaned_data[password]) user.save() code self.cleaned_data.get(family_code) if code: family Family.objects.filter(invite_codecode).first() if not family: raise forms.ValidationError(邀请码不存在) else: family Family.objects.create(namef{user.username}的家庭, invite_codeself._generate_code()) Profile.objects.create(useruser, familyfamily) return user这段代码里参数说明很重要set_password必须调用否则数据库里存明文密码登录永远失败first()而不是get()是因为查询不到时不抛异常返回 None 便于走后续逻辑Profile.objects.create是绑定动作用一个 profile 表把 User 和 Family 关联起来这是 Django 里做 OneToOne 扩展的常见姿势。3.2 分类树、记账提交和分页核心流程的三个隐藏细节记账页面的核心是一个表单选类型支出/收入、填金额、选分类、写备注。分类选择框需要渲染成树形下拉或者两层联动下拉这里要处理的是「父分类不可选」的问题——真实场景里你应该记账到「早餐」而不是「餐饮」这个父节点。实现上序列化分类树时给父节点加一个disabled标记from django.core.serializers.json import DjangoJSONEncoder import json def get_category_tree(user): categories Category.objects.filter(familyuser.profile.family).order_by(sort_order) tree [] for cat in categories: node {id: cat.id, name: cat.name, parent_id: cat.parent_id} tree.append(node) return json.dumps(tree, clsDjangoJSONEncoder)这里有个关键约束filter(familyuser.profile.family)缺一不可。如果你只写Category.objects.all()家庭 A 的人会看到家庭 B 的分类财务数据直接串家这种 bug 在联调前很难暴露因为各家庭分类名高度相似列表页根本看不出差异。记账提交后跳转到流水列表页这里必须处理分页。家庭记账的流水增长极快一个月几百条很正常不分页的话数据量大时页面渲染会卡顿影响体验。Django 自带Paginator但常见误用是「每次请求都分页、却不知道用了哪些字段排序」。正确的打开方式是from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger def transaction_list(request): txs Transaction.objects.filter(familyrequest.user.profile.family) txs txs.select_related(category).order_by(-occurred_at) paginator Paginator(txs, 20) # 每页 20 条 page request.GET.get(page) try: transactions paginator.page(page) except PageNotAnInteger: transactions paginator.page(1) except EmptyPage: transactions paginator.page(paginator.num_pages) return render(request, ledger/list.html, {transactions: transactions})select_related(category)不能省略否则列表页每行都执行一次分类查询20 条就是 21 条 SQL页面会多一个数量级的数据库开销这是新手最容易踩的性能坑。PageNotAnInteger和EmptyPage两个异常分别处理了「url 里 ?pageabc」和「?page999」的脏请求返回合理的默认页。另外还有一个容易忽略的点是删除交易记录时的确认机制。做家庭财务系统时要保留这一确认避免误删。直接删除的写法要慎重系统里点击删除按钮直接执行Transaction.objects.filter(idid).delete()不会有任何后悔药所以「二次确认」不是可选项而是必备逻辑。4. 避坑记录拆这套系统时踩过的五个坑4.1 分页计数器失效总条数显示为零现象列表页底部显示「共 0 条记录」但页面上明明有数据。原因模板里用了{{ transactions.count }}而transactions是Page对象而不是 QuerySetPage 对象没有count方法模板渲染时静默吞掉错误输出 0。解决总条数从paginator.count取或者{{ transactions.paginator.count }}。分页相关模板变量要区分清楚paginator.count是总量page.object_list是当前页数据page.has_previous、page.has_next判断前后页是否存在。4.2 数据库锁死报错database is locked现象点击保存后抛OperationalError: database is locked过一会儿又自己好了。原因SQLite 在高并发写入时不支持同时写默认设置超时时间短多个请求同时写库比如提交记账一瞬间多人操作就容易触发锁。开发环境下这个概率低但项目部署到服务器上后就开始出现。解决开发环境把 settings.py 里 DATABASES 的 OPTIONS 加上{timeout: 20}并开启 WAL 模式# settings.py DATABASES { default: { ENGINE: django.db.backends.sqlite3, NAME: BASE_DIR / db.sqlite3, OPTIONS: { timeout: 20, }, } }配合在数据库连接后执行PRAGMA journal_modeWAL;SQLite 并发写性能会好很多。如果部署到真实家用环境数据量不大这个配置够用量再大就该换 MySQL 了。4.3 收支统计对不上账时区设置把日期算错了现象月底看统计支出总额比实际记录少了一部分或者某一天的数据跑到前一天去。原因occurred_at用DateTimeField默认按 settings.py 里的TIME_ZONE存储 UTC 时间而前端提交的是本地时间比如 UTC8。用户晚上 11 点记的账存进数据库后变成了当天的 15 点日终汇总时「今天」的边界就错位了。解决核对 settings.py 里USE_TZ True时必须同时保证TIME_ZONE Asia/Shanghai并且前端表单传值用本地时间。更稳妥的做法是统一后端处理视图里接收时间字符串后用timezone.make_aware(datetime, timezone.get_current_timezone())转成带时区的时间再存库。每次做范围查询时都查occurred_at__date而不是occurred_at避免字符串隐式转换带来的边界问题。4.4 模板显示图片挂掉static 文件路径失效现象用户头像和图标在本地开发时正常显示部署到服务器或换目录启动后全部变成 broken image。这也对应网上高频的「vscode 里写 img 标签在 django 的 static 文件中显示不了」的问题。原因模板里写死了/static/img/avatar.png但项目部署后STATIC_URL变了或者模板没有{% load static %}直接写相对路径。Django 在 DEBUGFalse 时根本不会自动路由静态文件路径需要由 Web 服务器接管静态资源处理。解决模板头部加上{% load static %}引用处统一写{% static img/avatar.png %}部署阶段在 urls.py 里追加from django.conf import settings from django.conf.urls.static import static urlpatterns [...] if settings.DEBUG: urlpatterns static(settings.STATIC_URL, document_rootsettings.STATICFILES_DIRS[0])生产环境记得把 collectstatic 后的目录交给 Nginx 之类的静态服务器去 serve别让 Django 进程去扛。4.5 预算超支标记失灵预算统计跨了多个分类层级现象设置了「餐饮」预算 2000 元实际「早餐」「外卖」「聚餐」分类下已经花了 1800但页面没有提示超支。原因预算查询只统计了「餐饮」这个父分类直接关联的流水而实际记账都记在子分类上父分类的流水永远为空。解决统计预算进度时把预算分类及其所有子分类的 ID 捞出来做category_id__in查询def get_budget_spent(budget): cat_ids [budget.category_id] child_ids Category.objects.filter(parent_idbudget.category_id).values_list(id, flatTrue) cat_ids.extend(child_ids) qs Transaction.objects.filter( familybudget.family, category_id__incat_ids, transaction_typeexpense, occurred_at__monthtimezone.now().month ) total qs.aggregate(totalSum(amount))[total] or 0 return total这里values_list(id, flatTrue)返回的是一个 list扩展进cat_ids后就能走__in查询。父子分类同时统计预算提示才会真实反映家庭账本。预算统计本身也需要注意如果只在页面加载时计算用户记账后需要刷新才更新——考虑到家庭使用场景这个延迟可接受。5. 一个月维护下来的尾技备份、CSV 导入与查询调优5.1 用 dumpdata 做定期备份恢复时不踩编码坑家庭财务数据最怕丢。Django 自带的dumpdata命令可以导出全量数据不含数据库结构python3 manage.py dumpdata --exclude admin --exclude contenttypes --exclude auth.permission -o backup.json--exclude排除掉系统自动生成的表让备份文件更干净且主要聚焦业务数据恢复时间也缩短。恢复命令是python3 manage.py loaddata backup.json关键注意点dumpdata默认输出的 JSON 是 Unicode 转义形式传给loaddata时如果文件头有 BOM 或者混入了 Windows 的 CRLF 换行会直接报解析错误。解决方式是用python3直接处理或用iconv转成无 BOM 的 UTF-8。备份文件最好按日期命名保留最近 30 天的版本家庭财务管理不需要追求实时灾备但至少保底。5.2 银行流水 CSV 导入跳过表头、统一编码手工记账坚持不住是常态所以批量导入银行 CSV 是刚需功能。常见坑是编码银行导出的 CSV 多数是 GBK 或者 GB18030Python 默认按 UTF-8 读会直接抛UnicodeDecodeError。处理逻辑和代码是import csv def import_ledger_csv(file_obj, user): decoded file_obj.read().decode(gb18030) reader csv.DictReader(io.StringIO(decoded)) created 0 for row in reader: # 跳过空行和表头备注行 if not row.get(交易时间) or not row.get(金额): continue Transaction.objects.create( useruser, familyuser.profile.family, amountrow[金额], transaction_typeexpense if float(row[金额]) 0 else income, noterow.get(摘要, ), occurred_atparse_datetime(row[交易时间]), ) created 1 return createddecode(gb18030)能兼容绝大多数银行导出的编码比单纯用 gbk 更稳。金额列需要跳过负号或特殊符号时做一次replace。DictReader依赖 CSV 文件的表头字段名不同银行的列名不同导入前需要先确认映射关系更通用的方案是把表头映射做成字典参数例如{交易时间: trans_time, 金额: amount}这样换银行也不用改代码。5.3 查询调优索引之外的两个习惯家庭账本数据量上到几万条后列表页和统计页会有明显卡顿。除了前面说的联合索引还有两个习惯值得养成。第一个习惯是values()取代整表对象读取。统计图表只需要月份和金额两个字段没必要取一整行 Transaction 模型。写成下面这样会让数据库少做大量无用 IOmonthly Transaction.objects.filter(familyfamily_id) \ .values(occurred_at__month) \ .annotate(totalSum(amount)) \ .order_by(occurred_at__month)values后接字段名annotate做聚合返回的是一个轻量字典列表而不是 QuerySet 对象列表内存消耗和序列化开销都很小。对于页面性能有明显提升。第二个习惯是把图表数据吐成 JSON 接口而不是在 Django 模板里渲染。常见做法是在 views 里返回JsonResponse前端用 Chart.js 之类的库去拉数据画图。数据量再大一些的场景会考虑缓存家庭场景下用页面级缓存就够了没必要上重型方案。不过如果要在后台有数据时前端主动推送那就涉及 WebSocket 方案了对这个体量的项目来说属于过度设计不建议 Kotlin 或 Node 混进来Django 自带的分页加 JSON 接口已经够用。5.4 验证系统是否真正可用一条命令跑一套冒烟测试不知道这个压缩包里的代码能不能跑最快的验证方式是写一个冒烟测试脚本覆盖「注册 → 登录 → 记账 → 删账 → 看统计」全流程。放在应用的 tests.py 里然后执行python3 manage.py testpython3 manage.py test ledger -v 2from django.test import TestCase from django.urls import reverse from django.contrib.auth.models import User class SmokeTest(TestCase): def test_full_flow(self): # 注册用户 resp self.client.post(reverse(register), { username: test, password: pass12345, }) self.assertEqual(resp.status_code, 302) # 添加一笔支出 resp self.client.post(reverse(add_transaction), { amount: 88.50, transaction_type: expense, category: 1, note: 晚餐, }) self.assertEqual(resp.status_code, 302) # 验证列表页能查到这笔数据 resp self.client.get(reverse(transaction_list)) self.assertContains(resp, 88.50)关于测试数据的处理值得留意TestCase会自动为每个测试建独立测试库测试结束自动回滚不会污染真实账本所以可以放心跑。冒烟测试的价值不只是验证压缩包代码能用它本身也是这道课程设计里拿得出手的加分项——一个带测试的 Django 项目在答辩和后续扩展时都会顺利很多。从那以后我每拆一个 Django 系统都会先做三件事跑一遍迁移、写一条最小数据、备份一次数据库。这套家庭财务管理系统值得你按这个顺序重新过一遍。希望帮到你。本文还有配套的精品资源点击获取