简介基于Django框架和Python语言的家庭财务管理系统设计与实现源码包面向高校毕业设计、课程设计及Django实战学习者用于解决家庭财务数字化管理问题。系统涵盖账户管理、收支记录、预算控制、报表统计等核心模块并实现用户认证、权限管理、数据加密传输与备份等安全机制展示完整Web项目的分层设计思路。压缩包共收录2000个文件以JavaScript、HTML、CSS等前端资源为主JS与CSS负责交互和样式HTML构建页面结构配合Python后端源码与JSON配置整体约4.52MB目录结构清晰便于快速定位页面与逻辑。该资源已有41人浏览学习。读者可参考其中的Django与MySQL集成方式、前端模板组织方法、财务数据建模与模块划分技巧适用于课程实践、论文参考或自主开发同类系统能有效提升基于Django的全栈开发能力。1. 家庭财务系统不是记账本Django 这个包到底能给你什么拿到一个叫“基于Django的家庭财务管理系统设计.zip”的项目包很多人的第一反应是“又是个 CRUD 记账 Demo”。但真正做完这类项目你会发现家庭财务管理系统和普通记账本之间差着两个关键能力一是多成员、多账户、多分类的数据建模二是“钱花到哪里去了”的聚合分析。Django 的 ORM、Admin 后台和模板系统刚好把这两件事压缩到了很小的代码量里这也是这类项目在课程设计、毕业设计和个人练手里反复出现的原因。这个压缩包适合的读者很明确学过 Python 基础、想用 Django 快速做出一个“能跑、能展示、能写进简历”的完整项目的人。你能从里面拿到的不是记账逻辑——那太简单了——而是一套“从数据模型到统计报表再到部署打包”的完整链路。接下来我按自己复现这类项目的顺序把设计思路、核心代码、坑点和交付细节一次讲透。2. 先把数据模型立住账户、分类、账单和预算怎么建模2.1 为什么家庭财务系统要把“账户”和“分类”拆成独立模型很多人第一次写财务系统直接在账单表里放一个“收/支”字段再加个备注完事。这个设计在数据量小的时候看不出问题一旦你要回答“我这个月在餐饮上花了多少”“信用卡这个月还了多少”就会发现自己被锁死在一张扁平的表里怎么查都别扭。家庭财务系统的核心场景不是“记一笔”而是“算一笔”。要算得准账单必须挂靠到两个维度上钱从哪个账户出资产维度、钱花到哪个分类上用途维度。所以账户Account和分类Category一定要独立成表账单Transaction通过外键引用它们。这不仅是范式问题更是后续所有统计查询的地基。我一般把模型按“资产—收支—约束”三层来设计。资产层是账户表收支层是账单表约束层是预算表。账户表只管余额和类型现金、借记卡、信用卡、电子钱包账单表只管金额、日期、备注和两个外键预算表则按月绑定分类设置限额。三层各管各的互相之间只通过外键关联不混字段。# models.py 核心模型设计 from django.db import models from django.utils import timezone class Account(models.Model): # 账户类型用 IntegerField choices比单独建表更轻量 ACCOUNT_TYPES [ (1, 现金), (2, 借记卡), (3, 信用卡), (4, 电子钱包), ] name models.CharField(账户名称, max_length50) account_type models.IntegerField(账户类型, choicesACCOUNT_TYPES, default1) balance models.DecimalField(当前余额, max_digits12, decimal_places2, default0) created_at models.DateTimeField(auto_now_addTrue) def __str__(self): return f{self.name}{self.get_account_type_display()} class Category(models.Model): # 分类用 parent 自关联实现两级结构餐饮 - 外卖 CATEGORY_TYPES [ (1, 支出), (2, 收入), ] name models.CharField(分类名称, max_length50) category_type models.IntegerField(分类类型, choicesCATEGORY_TYPES, default1) parent models.ForeignKey(self, nullTrue, blankTrue, on_deletemodels.CASCADE, verbose_name父分类) sort_order models.IntegerField(排序, default0) class Meta: ordering [category_type, sort_order] def __str__(self): return self.name class Transaction(models.Model): account models.ForeignKey(Account, on_deletemodels.CASCADE, verbose_name账户) category models.ForeignKey(Category, on_deletemodels.PROTECT, verbose_name分类) amount models.DecimalField(金额, max_digits12, decimal_places2) date models.DateField(交易日期, defaulttimezone.localdate) note models.CharField(备注, max_length200, blankTrue) created_at models.DateTimeField(auto_now_addTrue) class Meta: ordering [-date, -id] indexes [ models.Index(fields[date, category]), ]这里有几个设计细节值得展开说。金额字段我坚持用 DecimalField 而不是 FloatField财务数据不允许二进制浮点的精度误差Python 的 float 在涉及累加时会出现 0.10.2 不等于 0.3 的问题写进账单表就是灾难。日期字段用 DateField 而不是 DateTimeField因为家庭账单的统计粒度到天就够用 DateTimeField 反而会在“当天收支汇总”时引入时区边界问题。外键的 on_delete 策略也要刻意区分账单删掉时连带删账户是合理的账户没了账单没意义但分类被账单引用时应该禁止删除或保留账单。所以我给 Transaction.category 用的是 PROTECT而不是默认的 CASCADE——否则你删一个“餐饮”分类全家的外卖记录一起消失这个翻车场景后面避坑章节会细说。2.2 预算表怎么设计才能支持“本月还剩多少”这类查询预算Budget表是家庭财务系统区别于普通记账本的关键表。它的作用是回答“我这个月预算还剩多少”这需要把预算按“年-月-分类”三个维度唯一化。最常见的失败设计是只存一个“月预算总额”这样你只能回答一个问题总超没超。而家庭场景里更常问的是“餐饮超了但交通没超”所以预算必须绑到分类上。class Budget(models.Model): category models.ForeignKey(Category, on_deletemodels.CASCADE, verbose_name预算分类) year models.PositiveIntegerField(年份) month models.PositiveIntegerField(月份) amount models.DecimalField(预算金额, max_digits12, decimal_places2) created_at models.DateTimeField(auto_now_addTrue) class Meta: # 同一分类同一个月只有一条预算记录防止重复录入 unique_together (category, year, month) ordering [year, month, category] def __str__(self): return f{self.year}-{self.month} {self.category.name} 预算 {self.amount}unique_together 是这里最关键的约束。没有它同一个分类在一个月内可以被插入多条预算查询时就要做去重或累加写出来的统计代码会异常丑陋。有这个约束在Django 的 ORM 会直接拦截重复插入省掉一层业务判断。预算的“剩余可用”不需要存字段而是实时算当月该分类的支出总和减去预算金额。这也意味着预算表的查询一定伴随着聚合操作。我建议在视图层封装一个工具函数不要在模板里做加减法。模板里做运算不仅难调试还会把业务逻辑泄露到展示层。# services.py 预算剩余计算 from django.db.models import Sum from django.utils import timezone def get_budget_status(year, month, category_id): 返回某分类在某月的预算使用情况 budget Budget.objects.filter( yearyear, monthmonth, category_idcategory_id ).first() if not budget: return None spent Transaction.objects.filter( category_idcategory_id, date__yearyear, date__monthmonth, category__category_type1, # 只统计支出 ).aggregate(totalSum(amount))[total] or 0 return { budget: budget.amount, spent: spent, remaining: budget.amount - spent, percent: float(spent / budget.amount * 100) if budget.amount else 0, }这里有两个参数细节。date__year 和 date__month 是 Django ORM 的日期字段查询语法它会把 date 字段按年、月做提取后过滤不需要你先从数据库里把全量数据捞出来再 Python 过滤。Sum 聚合遇到空结果集返回 None所以要用 or 0 兜底否则 remaining 会变成 None模板里一减就报错。3. 把记账和统计做成能用页面视图、模板和图表怎么配合3.1 用类视图还是函数视图家庭财务系统的选择逻辑Django 项目里视图有两种写法函数视图FBV和类视图CBV。家庭财务这类 CRUD 密集、但又有大量自定义统计逻辑的项目我建议混用标准的增删改用类视图统计报表和 dashboard 用函数视图。类视图帮你在 CreateView、UpdateView 里省掉表单渲染和校验的样板代码而统计页面的数据组装逻辑过于定制化硬套 TemplateView 反而要把代码塞进 get_context_data 里可读性更差。# views.py 记账功能用类视图统计用函数视图 from django.views.generic import CreateView, ListView from django.urls import reverse_lazy from .models import Transaction class TransactionCreateView(CreateView): model Transaction fields [account, category, amount, date, note] template_name finance/transaction_form.html success_url reverse_lazy(transaction_list) def form_valid(self, form): # 在表单校验通过后、入库前可以在这里插入额外逻辑 return super().form_valid(form) class TransactionListView(ListView): model Transaction template_name finance/transaction_list.html paginate_by 20 def get_queryset(self): # 支持按月份过滤URL 里 ?month2025-06 这样的参数 month self.request.GET.get(month) qs Transaction.objects.select_related(account, category) if month: year, mon month.split(-) qs qs.filter(date__yearyear, date__monthmon) return qsTransactionListView 里的 select_related 是个容易被忽略的优化点。Transaction 表有 account 和 category 两个外键模板里每显示一行就要访问一次关联对象。如果不加 select_relatedDjango 会对每一行额外发一次 SQL 查询20 条账单就是 41 次查询。加上之后只需要 1 次这就是经典的 N1 查询问题。新手项目数据量小感觉不到等账单攒到几千条页面加载会肉眼可见地变慢。表单里我直接用了 fields 列表而不是 ModelForm 类这是 Django 的快捷方式适合项目里没有复杂表单校验逻辑的场景。如果后续要加“金额必须大于 0”这类校验再单独定义 ModelForm类视图的 form_valid 钩子给了你一个不用改视图结构就能插入逻辑的位置。3.2 月度收支统计怎么用 ORM 聚合一次算完统计页面是家庭财务系统的门面它要回答的问题通常是本月总收入多少、总支出多少、各分类占比多少、最近几个月的趋势怎么样。这些问题全部可以靠 ORM 的 annotate 和 aggregate 组合解决不需要写一条原生 SQL。# views.py 月度统计视图 from django.db.models import Sum, Count, F from django.db.models.functions import TruncMonth from django.utils import timezone from datetime import date def dashboard(request): today timezone.localdate() current_month today.replace(day1) # 本月收支汇总 month_summary Transaction.objects.filter( date__yeartoday.year, date__monthtoday.month ).values(category__category_type).annotate( totalSum(amount) ) # 按分类统计本月支出按金额倒序取前 8 个 category_stats ( Transaction.objects .filter(date__yeartoday.year, date__monthtoday.month, category__category_type1) .values(category__name) .annotate(totalSum(amount)) .order_by(-total)[:8] ) # 最近 6 个月的收支趋势 trend ( Transaction.objects .filter(date__gtecurrent_month.replace(monthcurrent_month.month - 5)) .annotate(month_labelTruncMonth(date)) .values(month_label) .annotate( incomeSum(amount, filterF(category__category_type) 2), outcomeSum(amount, filterF(category__category_type) 1), ) .order_by(month_label) ) context { today: today, month_summary: month_summary, category_stats: category_stats, trend: list(trend), } return render(request, finance/dashboard.html, context)这段代码里最有含金量的是 TruncMonth 和 Sum 带 filter 的组合。TruncMonth 把日期字段截断到月份精度等价于 SQL 里的 DATE_TRUNC(month, date)它让按月分组可以直接在数据库里完成不需要先取全部数据再在 Python 里按月份 groupby。Sum 的 filter 参数是 Django 2.0 以后的能力可以在同一个分组里做条件求和避免你为了收入和支出拆两条查询再手动合并。F 对象在这里用来在查询语句里引用字段值和分类表的 category_type 做比较。month_label 返回的是一个 date 对象模板里要用 {{ item.month_label|date:Y-m }} 格式化直接输出会带着 00:00:00。另外查询里用了向后推 5 个月取 6 个月数据1 月的往前推会落在去年Django 的日期运算会自动处理跨年但 replace(month...) 在月份为 1 时往前推会报错我代码里这个写法在 1 月会出 bug实际项目里要改用 relativedelta 或手工处理跨年这个坑后面排查章节单独说。3.3 图表用 Chart.js 还是 ECharts给 Django 模板喂 JSON统计页面的图表渲染有两个流派服务端渲染图表用 matplotlib 生成图片和前端渲染图表用 JS 库读取 JSON 数据。家庭财务系统我强烈推荐前端渲染而且优先选 Chart.js 而不是 ECharts。原因很简单Chart.js 体积小、API 直观、对普通柱状图和饼图的支持足够好而 ECharts 的优势在复杂交互图表家庭财务用不上。!-- templates/finance/dashboard.html 图表部分 -- {% extends base.html %} {% block content %} div classrow div classcol-md-6 canvas idcategoryChart height200/canvas /div div classcol-md-6 canvas idtrendChart height200/canvas /div /div script srchttps://cdn.jsdelivr.net/npm/chart.js4/script script // 把 Django 视图传过来的 Python 数据结构转成 JSON const categoryData {{ category_stats|safe }}; const trendData {{ trend|safe }}; // 分类饼图 new Chart(document.getElementById(categoryChart), { type: pie, data: { labels: categoryData.map(item item.category__name), datasets: [{ data: categoryData.map(item parseFloat(item.total)), backgroundColor: [#ff6384, #36a2eb, #ffce56, #4bc0c0, #9966ff, #ff9f40, #c9cbcf, #7c8b9a] }] } }); // 月度趋势柱状图 new Chart(document.getElementById(trendChart), { type: bar, data: { labels: trendData.map(item item.month_label.slice(0, 7)), datasets: [ { label: 收入, data: trendData.map(item parseFloat(item.income || 0)), backgroundColor: #36a2eb }, { label: 支出, data: trendData.map(item parseFloat(item.outcome || 0)), backgroundColor: #ff6384 } ] } }); /script {% endblock %}这里最关键的 Django 知识点是 {{ category_stats|safe }}。Django 模板默认对变量做 HTML 转义QuerySet 转成字符串后会被转义成一堆 之类的实体图表直接没法解析。|safe 过滤器告诉 Django“这个变量是可信的不要转义”。但要注意|safe 只对结构简单的数据安全——如果你的数据里有用户输入的备注字段直接 safe 会造成 XSS 注入漏洞。这个页面里我传的 category__name 和 month_label 都是系统预设值不涉及用户输入所以安全。图表数据里 item.total 是 Decimal 对象JSON.stringify 不能直接处理所以 JS 里要用 parseFloat 转成数字。views 里也可以先把 Decimal 转成 float 再传模板但那会丢失金额精度前端展示没问题如果图表还要支持点击下钻看明细还是建议保留 Decimal 到后端。4. 从 .zip 到能跑的服务项目结构和部署配置怎么安排4.1 Django 项目目录怎么组织才能让接手的人不骂娘一个压缩包交付的项目别人解压后第一件事是看目录结构。如果所有代码堆在一个文件夹里、没有 requirements.txt、没有 README这个项目的印象分直接归零。我见过的课程设计和毕业设计里最常见的问题是大家为了“能跑”把配置写死在 settings.py 里数据库用 SQLite上传到别人机器上各种路径报错。我一般按下面的结构组织这也是 Django 官方项目结构的一个实用扩展。manage.py、项目配置目录、应用目录、templates、static 和 docs 各归其位。这个结构的目的不是好看而是让“解压、装依赖、起服务”三步走不卡壳。family_finance/ ├── manage.py # Django 项目管理入口 ├── requirements.txt # 依赖清单pip install -r 一键装 ├── README.md # 项目说明功能、启动步骤、默认账号 ├── config/ # 项目配置目录settings、urls │ ├── __init__.py │ ├── settings.py │ ├── urls.py │ └── wsgi.py ├── finance/ # 核心应用模型、视图、模板 │ ├── __init__.py │ ├── models.py │ ├── views.py │ ├── urls.py │ ├── admin.py │ ├── services.py # 预算计算等业务逻辑 │ ├── migrations/ │ └── templates/finance/ ├── static/ # 全局静态文件 │ ├── css/ │ ├── js/ │ └── images/ ├── docs/ # 设计文档、ER 图、接口说明 └── db.sqlite3 # SQLite 数据库开发环境这里有一个新手经常搞错的概念static 目录和每个 app 下的 static 目录的区别。finance 应用里的模板用到的 CSS、JS 如果只属于这个应用放在 finance/static/finance/ 下全局共用的样式放在项目根目录的 static/ 下并且要在 settings.py 里用 STATICFILES_DIRS 声明。Django 的静态文件查找机制是先查每个 app 的 static再查 STATICFILES_DIRS 声明的目录顺序别搞混。4.2 requirements.txt 和迁移数据交付包的两个保命文件requirements.txt 是压缩包交付里最重要的文件没有之一。它决定了别人能不能一小时内跑起你的项目。生成方式很简单在项目根目录执行 pip freeze requirements.txt。但 pip freeze 有个坑它会把当前 Python 环境里所有包都导出来包括你项目根本没用到的一些全局包这会让依赖体积膨胀。所以我会手动维护 requirements.txt只写项目真正用到的包。# 在项目根目录执行生成/更新依赖清单 pip freeze requirements.txt # 查看当前项目安装了哪些 django 相关包 pip list | grep -i djangorequirements.txt 内容示例版本号按实际安装版本写不要用“最新版”这种模糊表述。别人拿到你的压缩包后第一件事就是建虚拟环境、pip install -r requirements.txt版本号精确才能复现你的运行环境。Django4.2,5.0这个写法是告诉 pip 装 4.2 系列的最新版本而不是固定 4.2.0因为 4.2 系列的小版本修了很多 bug。如果你的项目里用了 django-filter 或 django-debug-toolbar 这类第三方扩展再加进去。用环境隔离跑起来是基本功python -m venv venv然后 source venv/bin/activateWindows 是 venv\Scripts\activate。数据迁移是另一个交付时的关键点。如果你在开发环境里录入了演示数据别人解压后直接打开就能看到图表效果这比空数据库的体验好太多。Django 的 dumpdata 和 loaddata 就是干这个的。# 导出核心数据到 fixtures保证数据可复现 python manage.py dumpdata finance --indent 2 finance/fixtures/seed_data.json # 别人拿到项目后先迁移表结构再加载演示数据 python manage.py migrate python manage.py loaddata seed_data.jsondumpdata 的 finance 参数指定只导出 finance 应用的数据不导出 Django 自带的 session、admin 日志等无关内容。--indent 2 让导出的 JSON 有缩进方便人眼检查。fixtures 目录需要手动创建loaddata 会自动在各 app 的 fixtures 目录里查找文件。这里有个坑dumpdata 默认会导出 id 作为主键如果别人数据库里已经有数据loaddata 会因为主键冲突报错。所以演示数据最好在空库上 load或者 dumpdata 时用 --natural-foreign 让外键用自然键而不是数字 id。4.3 开发环境和生产环境DEBUG 开关、静态文件和 gunicorn 基础配置压缩包里的项目如果只能 python manage.py runserver 跑那它的价值就打折了。一个完整的设计交付至少包含生产部署的说明关闭 DEBUG、收集静态文件、用 gunicorn 跑 Django、用 nginx 反代。settings.py 里相关的配置有几个必调项。# settings.py 生产环境的必要配置 import os # 生产环境必须设为 False否则错误堆栈会暴露给访客 DEBUG False # 允许访问的域名列表部署时改成你的域名或服务器 IP ALLOWED_HOSTS [your-domain.com, your-server-ip] # 静态文件收集后的存放目录nginx 从这里直接服务 STATIC_ROOT os.path.join(BASE_DIR, staticfiles) # URL 前缀确保模板里的 static 标签生成正确路径 STATIC_URL /static/ # 开发环境还要声明额外的静态文件目录 STATICFILES_DIRS [ os.path.join(BASE_DIR, static), ]DEBUGFalse 之后Django 不再自动服务静态文件必须执行 python manage.py collectstatic 把各 app 的 static 目录和 STATICFILES_DIRS 里的文件全部复制到 STATIC_ROOT 下。这个步骤漏掉的话你的页面会变得“裸奔”——CSS 全部加载不出来。这也是热搜词里“vscode写img标签在django的static文件中显示不了”这个问题的根源之一后面排查章节细讲。生产环境的启动命令是 gunicorn 加 wsgi 入口四个核心参数workers 进程数、bind 绑定地址、timeout 超时、accesslog 和 errorlog 日志路径。worker 数一般按 CPU 核数乘 2 加 1 估家庭财务这种轻量项目 2~4 个 worker 足够。# 生产环境启动命令示例gunicorn gunicorn config.wsgi:application \ --bind 0.0.0.0:8000 \ --workers 3 \ --timeout 60 \ --access-logfile /var/log/family_finance/access.log \ --error-logfile /var/log/family_finance/error.logconfig.wsgi:application 里的 config 是项目配置包的目录名wsgi 是其中的 wsgi.py 文件名application 是文件里的 WSGI 应用对象。这个路径如果搞错gunicorn 起不来最常见的报错是 ModuleNotFoundError 或 AttributeError。部署的时候 Django 的 SECRET_KEY 必须从 settings.py 里拿出来放到环境变量里写在代码里等于把密钥公开在压缩包里别人拿到就能伪造 session。5. 绕开 5 个高频坑从 static 加载失败到日期偏移5.1 static 文件加载不出来三个配置没对齐现象页面 HTML 能打开但没有任何样式浏览器控制台一大堆 404全是 .css 和 .js 文件的请求失败。原因分三种。第一种是 DEBUGTrue 时 STATICFILES_DIRS 没配Django 找不到你放在项目根目录 static/ 下的文件只能找到各 app 内 static 目录里的文件。第二种是 DEBUGFalse 时忘记 collectstaticSTATIC_ROOT 目录不存在或为空生产服务器没东西可服务。第三种是模板里写死路径而不是用 static 模板标签——你写了 /static/css/style.css但 STATIC_URL 改了路径就全错。解决# 确认三个配置项对齐 # settings.py 里检查 STATIC_URL、STATICFILES_DIRS、STATIC_ROOT 是否存在且路径正确 # DEBUGFalse 模式下必须执行 python manage.py collectstatic --noinput模板里引用静态文件用 {% load static %} 加 {% static css/style.css %}这样 STATIC_URL 改了多少都不用动模板。img 标签同理。这是 Django 官方推荐写法不要用写死的 /static/ 路径。5.2 账单日期偏移一天时区不是玄学是 UTC 和本地时间打架现象晚上 11 点记一笔账日期显示成第二天。原因settings.py 里 USE_TZTrue 时Django 把所有时间按 UTC 存储和运算。你用 Python 的 datetime.now() 拿到的是本地时间存进数据库被 Django 转成了 UTC取出时再转回当地时间。你本地 23:00 在东八区已经是 UTC 的第二天 15:00所以日期多了一天。解决获取当前日期用 Django 提供的 timezone.localdate()不要用 datetime.now()。代码里创建 Transaction 时 date 字段直接传 localdate()Django 会正确处理本地时区。settings.py 里还得配置 TIME_ZONE Asia/Shanghai这一步漏掉的话即使代码用 localdate() 也会按 UTC 计算偏移。5.3 删除分类连带删掉所有账单on_delete 选错了现象在 Django Admin 后台删除一个“餐饮”分类弹窗提示要删掉关联的 300 条记录你点了确认餐饮分类没了所有外卖记录也没了。原因ForeignKey 的 on_delete 参数用了默认的 CASCADE删除关联表记录时会把引用它的记录一并删除。分类是账单的“维度”不是账单的“容器”它的删除不应该级联删除账单。解决把 Transaction.category 外键的 on_delete 改成 PROTECT。PROTECT 会在有账单引用分类时阻止删除并抛出 ProtectedError强制你先处理这些账单改分类或删除再重分类。另一种思路是把分类的 is_active 置为 False 做软删除保留历史数据的完整统计口径。软删除要用模型字段和一个默认过滤器配合工作量略大但更稳妥。5.4 循环删除对象N1 查询在删除操作上的翻车版现象批量删除 1000 条账单程序跑了十几秒才完成数据库连接差点超时。原因在 Python 里循环调用 delete() 或 filter()每调用一次发一条 SQL1000 条就是 1000 次 DELETE 查询。这个问题的本质和 select_related 解决的 N1 是同一个只是方向从查询变成删除。解决用 QuerySet 的批量删除方法一条 DELETE 语句解决。# 批量删除指定月份的所有支出账单 Transaction.objects.filter( date__year2025, date__month6, category__category_type1, ).delete() # 这只会发 1 条 DELETE SQL而不是 N 条这里的 delete() 是 QuerySet 的批量方法返回一个二元组 (删除总数, 明细字典)。它不是模型实例的方法所以要先 filter 得到 QuerySet 再调 delete。注意 delete() 是硬删除执行后没有后悔药保险起见可以先把要删除的数据导出备份再删。5.5 DecimalField 精度和前端显示float 不是自由是地雷现象图表上显示本月支出 1234.5600000001 元数据库里明明是 1234.56。原因查询聚合结果被 Python 转成 float 的过程中产生了二进制浮点误差或者模板渲染时 Decimal 对象被转成字符串但精度没控制。另一个常见场景是 JS 读 JSON 数据时把金额当数字解析Decimal 的 1234.56 转成 JSON 后变成 1234.56图表看不出问题但传到后端再转回来就可能多出尾巴。解决数据从 Django 视图传到模板时金额和统计结果保持 Decimal 类型模板里用 {{ value|floatformat:2 }} 格式化成两位小数。转 JSON 给 Chart.js 时在 views 里对金额字段 round(x, 2) 处理# 转 JSON 前统一做金额格式化 category_data [ {name: item[category__name], total: round(float(item[total]), 2)} for item in category_stats ]调用 float() 是不可避免的因为 JSON 标准里没有 Decimal但 float() 之前的 Decimal 累加已经精确完成只损失转 float 那一刻的精度。round 到 2 位小数后前端展示和回传都不会出幺蛾子。5.6 日期跨年或半月统计month.replace 在 1 月直接抛异常现象1 月份打开 Dashboard 页面直接 500错误信息是 ValueError: month must be in 1..12。原因代码里用了 current_month.replace(monthcurrent_month.month - 5)。1 月减 5 得到 -4Python 的 date.replace 不接受负数月份。解决用 dateutil 的 relativedelta 替代 replace 做月份加减。from dateutil.relativedelta import relativedelta # 向后推 5 个月自动处理跨年 start_date current_month - relativedelta(months5) trend ( Transaction.objects .filter(date__gtestart_date) .annotate(month_labelTruncMonth(date)) .values(month_label) .annotate(...) )relativedelta 处理 month 加减时自动处理年份进位2 月减 5 会得到前一年的 9 月。这个库不是 Django 自带的需要加入 requirements.txt。没有它的场景可以手工写算法当前月数减 5 小于 1 时年份减 1、月份加 12。6. 交付前的最后一道工序验证数据一致性和打包检查清单压缩包交付前我习惯跑一遍完整的“验收脚本”而不是直接 zip 整个文件夹。这个脚本包含三类检查数据一致性、查询性能、配置安全。数据一致性检查是确认账目算法没算错——比如所有账户余额变化之和等于所有交易额之和这是财务系统最基本的不变式。# Django shell 里跑数据一致性校验确认账实相符 python manage.py shell -c from django.db.models import Sum from finance.models import Account, Transaction # 校验 1所有账户余额之和应等于期初余额加所有交易净额如果系统从零开始 total_balance Account.objects.aggregate(totalSum(balance))[total] total_income Transaction.objects.filter(category__category_type2).aggregate(sSum(amount))[s] or 0 total_outcome Transaction.objects.filter(category__category_type1).aggregate(sSum(amount))[s] or 0 print(f账户余额合计: {total_balance}) print(f累计收入: {total_income}, 累计支出: {total_outcome}) print(f净额: {total_income - total_outcome}) # 校验 2检查是否有金额为 0 或负数的异常账单 bad Transaction.objects.filter(amount__lte0).count() print(f金额异常账单: {bad}) 这段命令用 -c 参数直接传入 shell 代码不需要建脚本文件。aggregate 加 filter 的组合在千万元素级别性能没压力家庭财务的数据量根本到不了瓶颈。检查输出里如果净额不等于余额的 0 或者出现负数账单说明某笔交易录错了要在交付前修数据否则用户解压后第一眼看到脏数据体验直接崩掉。第二类检查是确认项目没有把敏感信息带进压缩包。检查 settings.py 里的 SECRET_KEY 是否是刚生成的随机值而不是默认值检查是否忘记把 DEBUG 改回 False检查有没有把本机的绝对路径写死在代码里。路径写死是 Windows 开发者的高发问题代码里出现 C:\Users\xxx... 这种字符串别人解压到别的机器就懵了。第三类检查落在静态文件上python manage.py collectstatic --noinput 跑一遍确认没有收集错误再确认模板里所有 static 标签都能解析到文件。这时候打开页面按 F12 看 Network 面板确认 CSS、JS、图片全部 200没有红色 404。做完这三类检查压缩包才算配得上“设计”两个字。我自己的习惯是最后再跑一遍 python manage.py check --deploy这是 Django 官方提供的部署前检查命令会扫描 settings.py 里所有安全相关的配置问题比如 DEBUG 是否未关、SECRET_KEY 是否过短、是否缺安全头部配置。它输出的每条警告都值得看一眼虽然不是每条都必须修但至少让你知道风险在哪里。另外提一个进阶玩法如果项目里有 websocket 需求比如“后台新增账单后前端余额实时刷新”Django 的解决方案是 Django Channels它把 ASGI 引入 Django让视图层能处理长连接。但 Channels 的部署复杂度比 WSGI 高不少需要 Daphne 或 Uvicorn 配合还要在 nginx 里单独配置升级请求头。家庭财务系统如果只是自己家用轮询都比 websocket 省心——5 秒钟一次的 fetch 请求对本地服务毫无压力。所以别看到 websocket 就往上怼技术选型永远是场景说了算。这个方向值不值得做我的判断是值得。Django 的项目实战价值不在于 CRUD 而在于数据建模、聚合查询和部署运维这条链路家庭财务系统刚好把这些点全占了而且领域简单不用花时间去理解业务。我自己每带一个新人学 Django都建议拿财务系统练手——它能把 Django 的 ORM、Admin、模板、部署全部串起来做完一遍再看电商、CMS 这类项目核心思路都是通的。希望这篇笔记能帮你在拿到类似压缩包时不只解压跑通还能改得动、说得清、部署得上去。本文还有配套的精品资源点击获取
