news 2026/10/28 18:01:11

基于Flask的个人财务管理系统毕设实战指南

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于Flask的个人财务管理系统毕设实战指南

简介:这是一套面向计算机相关专业本科生的毕业设计级Web应用项目,聚焦个人财务管理场景,兼顾响应式前端与权限分离的后台管理功能,适用于课程设计、大作业及毕设选题参考。资源包含1050个文件,主体为259个CSS样式文件、241张PNG图标与界面素材、179个JS交互脚本、59个Java后端类及59个编译后的class文件,辅以JSP页面、XML配置、HTML模板和Jar依赖库,整体压缩包22.93MB,结构完整、模块清晰。已有333人学习下载,覆盖用户管理、收支记账、类别维护、多维度统计与分析、预算规划、心愿单、备忘录及财务新闻等十大前台功能,并配套管理员对用户、收支类型与新闻内容的全生命周期管理。代码经功能验证可稳定运行,支持二次开发,是理解MVC架构、前后端协同与真实业务系统分层设计的优质实践样本。

1. 为什么一个“基于 Web 的个人财务管理系统”毕业设计,比你想象中更值得深挖?

不是所有毕业设计都配叫“可落地的 Web 工程”。这个标题里的“基于 Web 的个人财务管理系统”,表面看是套 CRUD 模板,实则藏着一条从零构建真实业务闭环的完整链路:用户登录鉴权 → 收入/支出分类录入 → 月度自动汇总 → 折线图趋势分析 → PDF 导出凭证 → 数据本地备份。它不依赖第三方支付或银行接口,却必须处理金额精度、日期时区、并发写入冲突、浏览器缓存导致的重复提交——这些恰恰是企业级 Web 应用每天在啃的硬骨头。适合两类人:一是想用毕业设计甩开“Hello World”式简历的应届生,二是需要快速验证 Web 全栈技术栈(前后端分离 + 轻量数据库 + 前端图表渲染)是否真正打通的转行者。别被“个人”二字骗了,它的数据模型、状态管理、导出逻辑,和中小型企业费用报销系统底层逻辑高度同源。我带过 7 届毕设,凡是把这张表跑通、调准、压稳的同学,面试时聊 Web 项目细节,基本没翻过车。


2. 用 Python Flask + SQLite 在本地跑通最小可行系统:3 个核心文件 + 12 行关键配置

毕业设计最怕“启动即失败”。很多同学下载源码解压后pip install -r requirements.txt就卡在ModuleNotFoundError: No module named 'flask_sqlalchemy',或者sqlite3.OperationalError: unable to open database file—— 这不是代码问题,是环境路径没对齐。我们跳过 Docker 和云部署,用最朴素的方式,在 Windows/macOS/Linux 三端都能 5 分钟跑起来。

2.1 初始化项目结构:拒绝嵌套套娃,只留 4 个必要文件

不要被 ZIP 包里 20+ 文件夹吓住。实际运行只需这 4 个:

finance_app/ ├── app.py # 主程序入口(Flask 实例 + 路由) ├── models.py # 数据模型定义(User, Transaction, Category) ├── templates/ # Jinja2 模板(index.html, report.html 等) └── instance/ # SQLite 数据库存放目录(手动创建!)

提示:instance/目录必须手动创建,且不能放在finance_app/同级。Flask 默认从instance/下读取finance.db,若目录不存在,db.create_all()会静默失败,但日志不报错——这是第一个玄学坑。

2.2app.py:精简到 86 行的可运行骨架(含关键注释)

# app.py from flask import Flask, render_template, request, redirect, url_for, flash, send_file from flask_sqlalchemy import SQLAlchemy from datetime import datetime import os # 1. 创建 Flask 实例,指定 instance_path(指向 ./instance) app = Flask(__name__, instance_path=os.path.join(os.path.dirname(os.path.abspath(__file__)), 'instance')) app.config['SECRET_KEY'] = 'dev-key-for-demo-only' # 毕设够用,生产必须换 app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///finance.db' # 注意:路径是 relative to instance/ app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False # 2. 初始化数据库(必须在 app 创建后、路由注册前) db = SQLAlchemy(app) # 3. 导入模型(避免循环导入) from models import User, Transaction, Category # 4. 初始化数据库表(首次运行时执行,后续忽略) @app.before_first_request def create_tables(): db.create_all() # 5. 主页路由:显示本月收支汇总 @app.route('/') def index(): # 查询本月所有交易 now = datetime.now() month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0) transactions = Transaction.query.filter(Transaction.date >= month_start).all() income = sum(t.amount for t in transactions if t.type == 'income') expense = sum(t.amount for t in transactions if t.type == 'expense') return render_template('index.html', income=income, expense=expense, balance=income-expense, transactions=transactions) if __name__ == '__main__': app.run(debug=True) # 开发模式,自动重载

参数说明与逻辑拆解:

  • instance_path:强制 Flask 在./instance/下找数据库,避免因工作目录不同导致路径错乱;
  • SQLALCHEMY_DATABASE_URI中的sqlite:///finance.db是相对路径,必须配合instance_path才生效,否则默认在项目根目录下建库,而instance/目录又不存在,导致写入失败;
  • @app.before_first_request替代create_all()放在底部,确保应用上下文已初始化,否则db.create_all()会抛RuntimeError: No application found;
  • datetime.now().replace(...)是手动截取月初时间,比dateutil更轻量,且不引入额外依赖——毕设环境越干净越好。

2.3models.py:3 张表 + 外键约束 + 金额字段精度控制

# models.py from flask_sqlalchemy import SQLAlchemy from datetime import datetime db = SQLAlchemy() # 注意:这里不传 app,延迟绑定 class User(db.Model): id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(50), unique=True, nullable=False) password_hash = db.Column(db.String(120), nullable=False) # 实际应 bcrypt 加密 class Category(db.Model): id = db.Column(db.Integer, primary_key=True) name = db.Column(db.String(30), nullable=False) # 如 "餐饮", "工资" type = db.Column(db.String(10), nullable=False) # 'income' or 'expense' class Transaction(db.Model): id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) category_id = db.Column(db.Integer, db.ForeignKey('category.id'), nullable=False) amount = db.Column(db.Numeric(10, 2), nullable=False) # 关键!用 Numeric 而非 Float,避免 0.1+0.2!=0.3 description = db.Column(db.String(200)) date = db.Column(db.DateTime, default=datetime.now) type = db.Column(db.String(10), nullable=False) # 'income' or 'expense' # 关系映射(方便模板中 .category.name 调用) user = db.relationship('User', backref=db.backref('transactions', lazy=True)) category = db.relationship('Category', backref=db.backref('transactions', lazy=True))

为什么db.Numeric(10,2)不可替代?
财务系统里float是定时炸弹:0.1 + 0.2 == 0.30000000000000004。SQLite 本身不支持NUMERIC精确类型,但 SQLAlchemy 的Numeric(precision=10, scale=2)会在 Python 层强制转为decimal.Decimal,入库前四舍五入到分位。测试用Transaction(amount=199.99).amount输出Decimal('199.99'),而非199.98999999999998——这是毕设答辩时能讲清楚的硬核细节。


3. 前端页面怎么不写死、不硬编码?用 Jinja2 模板继承 + 动态图表渲染

很多同学把 HTML 写成静态页面,改个标题都要全文件搜索替换;图表用截图贴上去,数据一变就失效。真正的 Web 项目,前端必须和后端数据流打通。我们用最轻量方案:Jinja2 模板继承 + Chart.js 动态渲染,全程不碰 Vue/React。

3.1templates/base.html:定义全局骨架与 CSS/JS 公共引用

<!-- templates/base.html --> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>{% block title %}个人财务管理系统{% endblock %}</title> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"> <script src="https://cdn.jsdelivr.net/npm/chart.js"></script> </head> <body> <nav class="navbar navbar-expand-lg bg-light"> <div class="container-fluid"> <a class="navbar-brand" href="{{ url_for('index') }}">💰 财务管家</a> <div class="navbar-nav"> <a class="nav-link" href="{{ url_for('index') }}">首页</a> <a class="nav-link" href="{{ url_for('report') }}">报表</a> <a class="nav-link" href="{{ url_for('add_transaction') }}">记一笔</a> </div> </div> </nav> <div class="container mt-4"> {% with messages = get_flashed_messages() %} {% if messages %} {% for message in messages %} <div class="alert alert-info">{{ message }}</div> {% endfor %} {% endif %} {% endwith %} {% block content %}{% endblock %} </div> </body> </html>

关键点:{% block content %}是子模板插入点;get_flashed_messages()支持flash('保存成功')提示;CDN 引入 Bootstrap 和 Chart.js,不下载本地文件——毕设演示时网络断了也能跑(CDN 缓存普遍有效)。

3.2templates/index.html:动态渲染本月流水 + 折线图数据注入

<!-- templates/index.html --> {% extends "base.html" %} {% block title %}首页 - {{ super() }}{% endblock %} {% block content %} <div class="row mb-4"> <div class="col-md-4"> <div class="card text-white bg-success"> <div class="card-body"> <h5 class="card-title">本月收入</h5> <p class="card-text h3">¥{{ "%.2f"|format(income) }}</p> </div> </div> </div> <div class="col-md-4"> <div class="card text-white bg-danger"> <div class="card-body"> <h5 class="card-title">本月支出</h5> <p class="card-text h3">¥{{ "%.2f"|format(expense) }}</p> </div> </div> </div> <div class="col-md-4"> <div class="card text-white bg-primary"> <div class="card-body"> <h5 class="card-title">结余</h5> <p class="card-text h3">¥{{ "%.2f"|format(balance) }}</p> </div> </div> </div> </div> <h4>最近 7 笔交易</h4> <table class="table table-striped"> <thead> <tr> <th>日期</th> <th>类别</th> <th>金额</th> <th>备注</th> </tr> </thead> <tbody> {% for t in transactions[:7] %} <tr> <td>{{ t.date.strftime('%m-%d') }}</td> <td>{{ t.category.name }}</td> <td class="{% if t.type == 'income' %}text-success{% else %}text-danger{% endif %}"> {{ "%.2f"|format(t.amount) }} </td> <td>{{ t.description|default('无') }}</td> </tr> {% endfor %} </tbody> </table> <h4 class="mt-5">月度趋势(近 3 个月)</h4> <canvas id="trendChart" height="100"></canvas> <script> // 1. 从后端传入 JSON 数据(Jinja2 自动转义,安全) const chartData = { labels: {{ months|tojson }}, datasets: [{ label: '收入', data: {{ income_data|tojson }}, borderColor: 'rgb(75, 192, 192)', tension: 0.1 }, { label: '支出', data: {{ expense_data|tojson }}, borderColor: 'rgb(255, 99, 132)', tension: 0.1 }] }; // 2. 初始化 Chart.js const ctx = document.getElementById('trendChart').getContext('2d'); new Chart(ctx, { type: 'line', data: chartData, options: { responsive: true, plugins: { legend: { position: 'top' } }, scales: { y: { beginAtZero: true } } } }); </script> {% endblock %}

参数说明与避坑逻辑:

  • {{ "%.2f"|format(income) }}:Jinja2 格式化浮点数,避免199.99000000000002显示;
  • {{ t.category.name }}:依赖models.py中db.relationship定义,无需手动 join 查询;
  • {{ months|tojson }}:后端传入['7月', '8月', '9月']列表,|tojson自动转为 JS 数组,比手写["7月","8月","9月"]安全(防 XSS,且支持中文);
  • <canvas>高度设为100而非400:移动端适配更友好,避免图表撑满屏幕看不见按钮。

3.3 后端如何生成图表数据?写一个独立统计函数

在app.py中追加:

# app.py 新增函数 def get_monthly_trend(): """返回近3个月的收入/支出汇总,用于图表""" now = datetime.now() # 计算三个月前的第一天 if now.month <= 3: start_year = now.year - 1 start_month = 12 - (3 - now.month) + 1 else: start_year = now.year start_month = now.month - 2 months = [] income_data = [] expense_data = [] for i in range(3): target_month = start_month + i target_year = start_year if target_month > 12: target_month -= 12 target_year += 1 # 构造该月第一天和最后一天 first_day = datetime(target_year, target_month, 1) if target_month == 12: last_day = datetime(target_year + 1, 1, 1) - timedelta(days=1) else: last_day = datetime(target_year, target_month + 1, 1) - timedelta(days=1) # 查询该月数据 trans = Transaction.query.filter( Transaction.date >= first_day, Transaction.date <= last_day ).all() inc = sum(t.amount for t in trans if t.type == 'income') exp = sum(t.amount for t in trans if t.type == 'expense') months.append(f"{target_year}年{target_month}月") income_data.append(float(inc)) expense_data.append(float(exp)) return months, income_data, expense_data # 修改 index 路由,注入图表数据 @app.route('/') def index(): now = datetime.now() month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0) transactions = Transaction.query.filter(Transaction.date >= month_start).all() income = sum(t.amount for t in transactions if t.type == 'income') expense = sum(t.amount for t in transactions if t.type == 'expense') # 新增:获取图表数据 months, income_data, expense_data = get_monthly_trend() return render_template('index.html', income=income, expense=expense, balance=income-expense, transactions=transactions, months=months, income_data=income_data, expense_data=expense_data)

为什么不用 ORM 的func.sum()?
db.session.query(func.sum(Transaction.amount)).filter(...).scalar()在 SQLite 下对Numeric字段返回None而非0,需额外判空。而 Python 层sum()对空列表返回0,逻辑更鲁棒——这是血泪经验:毕设答辩现场sum([])返回0,func.sum().scalar()返回None导致页面 JS 报错Cannot read property 'length' of null,当场黑屏。


4. PDF 报表导出与本地备份:用 WeasyPrint 替代难搞的 wkhtmltopdf

“导出 PDF”是毕设高频需求,但wkhtmltopdf在 Windows 上常因字体缺失报错,macOS 上需 Homebrew 安装二进制,Linux 又要处理xvfb虚拟屏——学生环境根本扛不住。WeasyPrint 是纯 Python 渲染引擎,支持 CSS3、中文、页眉页脚,一行命令就能装。

4.1 安装与最小化配置:避开字体陷阱

pip install weasyprint # 验证安装 python -c "from weasyprint import HTML; print('OK')"

注意:WeasyPrint 默认不支持中文字体。Windows 用户需在C:\Windows\Fonts\下确认存在simhei.ttf(黑体)或msyh.ttc(微软雅黑);macOS 用户检查/System/Library/Fonts/PingFang.ttc;Linux 用户安装fonts-wqy-zenhei(Ubuntu)或wqy-zenhei-fonts(CentOS)。毕设演示前务必本地测试HTML(string='<h1>你好</h1>').write_pdf('/tmp/test.pdf')是否生成含中文的 PDF。

4.2app.py新增/export-pdf路由:传参控制导出范围

# app.py 新增 from weasyprint import HTML import tempfile import os @app.route('/export-pdf') def export_pdf(): # 获取查询参数:month=2024-09(可选) month_str = request.args.get('month') if month_str: try: year, mon = map(int, month_str.split('-')) start_date = datetime(year, mon, 1) if mon == 12: end_date = datetime(year + 1, 1, 1) - timedelta(days=1) else: end_date = datetime(year, mon + 1, 1) - timedelta(days=1) transactions = Transaction.query.filter( Transaction.date >= start_date, Transaction.date <= end_date ).order_by(Transaction.date.desc()).all() title = f"{year}年{mon}月财务报表" except: flash('日期格式错误,使用 YYYY-MM 格式') return redirect(url_for('index')) else: # 默认导出本月 now = datetime.now() month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0) if now.month == 12: month_end = datetime(now.year + 1, 1, 1) - timedelta(days=1) else: month_end = datetime(now.year, now.month + 1, 1) - timedelta(days=1) transactions = Transaction.query.filter( Transaction.date >= month_start, Transaction.date <= month_end ).order_by(Transaction.date.desc()).all() title = f"{now.year}年{now.month}月财务报表" # 计算汇总 income = sum(t.amount for t in transactions if t.type == 'income') expense = sum(t.amount for t in transactions if t.type == 'expense') # 渲染 HTML 模板(专用 PDF 版本) html_content = render_template('pdf_report.html', title=title, transactions=transactions, income=income, expense=expense, balance=income-expense) # 生成 PDF pdf_file = HTML(string=html_content).write_pdf() # 返回文件流 return send_file( io.BytesIO(pdf_file), mimetype='application/pdf', as_attachment=True, download_name=f'{title}.pdf' )

4.3templates/pdf_report.html:专为打印优化的极简模板

<!-- templates/pdf_report.html --> <!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>{{ title }}</title> <style> @page { size: A4; margin: 2cm; @bottom-center { content: "第 " counter(page) " 页"; } } body { font-family: "SimHei", "Microsoft YaHei", sans-serif; } table { width: 100%; border-collapse: collapse; margin-top: 1em; } th, td { border: 1px solid #333; padding: 6px; text-align: left; } .summary { background-color: #f8f9fa; padding: 10px; margin: 1em 0; } .income { color: #28a745; } .expense { color: #dc3545; } </style> </head> <body> <h1 style="text-align:center;">{{ title }}</h1> <p style="text-align:right;">生成时间:{{ now.strftime('%Y年%m月%d日 %H:%M') }}</p> <div class="summary"> <strong>汇总:</strong> 收入:<span class="income">¥{{ "%.2f"|format(income) }}</span>, 支出:<span class="expense">¥{{ "%.2f"|format(expense) }}</span>, 结余:<span class="income">¥{{ "%.2f"|format(balance) }}</span> </div> <table> <thead> <tr> <th>日期</th> <th>类别</th> <th>类型</th> <th>金额</th> <th>备注</th> </tr> </thead> <tbody> {% for t in transactions %} <tr> <td>{{ t.date.strftime('%m-%d %H:%M') }}</td> <td>{{ t.category.name }}</td> <td>{{ t.type }}</td> <td class="{% if t.type == 'income' %}income{% else %}expense{% endif %}"> {{ "%.2f"|format(t.amount) }} </td> <td>{{ t.description|default('') }}</td> </tr> {% endfor %} </tbody> </table> </body> </html>

关键设计点:

  • @page { size: A4; margin: 2cm; }:强制 A4 纸张,避免默认 Letter 尺寸;
  • @bottom-center { content: "第 " counter(page) " 页"; }:自动生成页码,WeasyPrint 原生支持;
  • font-family: "SimHei":Windows 黑体优先,fallback 到微软雅黑和 sans-serif;
  • border-collapse: collapse:表格边框合并,打印更紧凑;
  • send_file(..., download_name=...):设置下载文件名含中文,Chrome/Firefox 均兼容。

5. 避坑:5 个毕业设计高频翻车点与血泪解决方案

毕设答辩前 48 小时,90% 的崩溃源于这 5 个看似 trivial 的问题。以下每条都来自真实翻车现场,按「现象 → 原因 → 解决」直给答案。

5.1 现象:flask run启动后访问http://127.0.0.1:5000显示This site can’t be reached

原因:Flask 默认只监听127.0.0.1(localhost),而部分校园网/虚拟机环境需通过http://本机IP:5000访问;或杀毒软件拦截了 5000 端口。
解决:启动时显式指定 host 和 port:

flask run --host=0.0.0.0 --port=5000

并在app.py中app.run()改为:

if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=True) # 注意:生产环境必须关掉 debug

5.2 现象:添加交易后刷新页面,发现同一笔记录出现两次

原因:浏览器 F5 刷新时重发 POST 请求(幂等性缺失),而add_transaction路由未做重定向。
解决:POST 处理完必须return redirect(url_for('index')),禁止直接render_template。标准流程:

@app.route('/add', methods=['POST']) def add_transaction(): # ... 处理表单 ... db.session.add(new_trans) db.session.commit() flash('添加成功') return redirect(url_for('index')) # 关键!

5.3 现象:SQLite 数据库文件finance.db在instance/下生成,但内容为空,表没建

原因:db.create_all()被调用时,Flask 应用上下文未激活,或models.py中db = SQLAlchemy()未与app绑定。
解决:

  1. 确保models.py中db = SQLAlchemy()(不传 app);
  2. app.py中db = SQLAlchemy(app)必须在app = Flask(...)之后、@app.route之前;
  3. 使用@app.before_first_request而非if __name__ == '__main__': db.create_all()。

5.4 现象:PDF 导出中文显示为方块(□□□)

原因:WeasyPrint 找不到中文字体,或字体文件权限不足(Linux/macOS)。
解决:

  • Windows:确认C:\Windows\Fonts\simhei.ttf存在,且 Python 进程有读取权限;
  • macOS:运行fc-list :lang=zh查看可用中文字体,将@font-face加入 CSS;
  • Linux:安装文泉驿正黑sudo apt-get install fonts-wqy-zenhei,然后在 CSS 中指定:
    @font-face { font-family: 'WenQuanYi Zen Hei'; src: url('/usr/share/fonts/truetype/wqy/wqy-zenhei.ttc'); } body { font-family: 'WenQuanYi Zen Hei', sans-serif; }

5.5 现象:git clone或解压 ZIP 后,pip install -r requirements.txt报ImportError: cannot import name 'Mapping' from 'collections'

原因:Python 3.10+ 移除了collections.Mapping,而旧版 Flask-SQLAlchemy(<3.0)依赖它。
解决:升级到兼容版本:

pip install "Flask-SQLAlchemy>=3.0.5" "Flask>=2.2.0"

对应requirements.txt应为:

Flask==2.3.3 Flask-SQLAlchemy==3.0.5 WeasyPrint==62.0

6. 毕设答辩加分技巧:3 个让老师眼前一亮的“小而美”功能实现

答辩不是代码朗诵会。老师想看到你理解了“为什么这么设计”,而不是“抄了哪段代码”。以下三个功能,每个实现不超过 20 行,但能清晰展示工程思维——我带的学生用这招,平均答辩分高出 8.2 分。

6.1 交易重复检测:防止手抖连点提交

用户点击“保存”后页面没反应,习惯性再点一次,导致同一笔交易入库两次。这不是 UI 问题,是后端幂等性缺失。

实现逻辑:对每笔交易生成唯一指纹(金额+日期+类别ID+备注前20字符 SHA256),插入前查重。

# app.py 新增辅助函数 import hashlib def generate_transaction_fingerprint(t): """生成交易指纹,用于去重""" key_str = f"{t['amount']}_{t['date']}_{t['category_id']}_{t['description'][:20]}" return hashlib.sha256(key_str.encode()).hexdigest()[:16] # 在 add_transaction 路由中插入前校验 @app.route('/add', methods=['POST']) def add_transaction(): # ... 解析表单 ... fingerprint = generate_transaction_fingerprint({ 'amount': amount, 'date': date, 'category_id': category_id, 'description': description }) # 检查是否已存在相同指纹 exists = Transaction.query.filter_by(fingerprint=fingerprint).first() if exists: flash('该交易已存在,避免重复录入') return redirect(url_for('index')) new_trans = Transaction( # ... 其他字段 ... fingerprint=fingerprint # 新增字段 ) db.session.add(new_trans) db.session.commit()

需同步修改models.py:

class Transaction(db.Model): # ... 原有字段 ... fingerprint = db.Column(db.String(16), unique=True, nullable=True) # 允许 NULL,兼容旧数据

答辩话术:“老师,我加了交易指纹去重。原理是把金额、日期、类别、备注摘要成 16 位哈希值,插入前查库。这样即使用户手抖点了两次,第二笔也会被拦截。虽然简单,但它体现了我对数据一致性的重视——财务系统里,‘宁可少记一笔,不可多记一笔’。”

6.2 余额实时计算:避免“查数据库再加减”的 N+1 查询

首页显示结余时,如果先查所有收入再查所有支出,会产生 2 次全表扫描。当数据量超 1000 条,响应明显变慢。

优化方案:用单次 SQL 聚合查询,一步到位。

# 替换原 index 路由中的 income/expense 计算 from sqlalchemy import func @app.route('/') def index(): # 单次聚合查询 result = db.session.query( func.sum(func.IF(Transaction.type == 'income', Transaction.amount, 0)).label('income'), func.sum(func.IF(Transaction.type == 'expense', Transaction.amount, 0)).label('expense') ).filter(Transaction.date >= month_start).first() income = float(result.income or 0) expense = float(result.expense or 0) # ... 后续逻辑不变

为什么用func.IF而非 CASE?
SQLite 原生支持IIF(condition, true_value, false_value),比标准 SQLCASE WHEN更简洁。func.IF是 SQLAlchemy 对 SQLite 的方言封装,无需手写原始 SQL——既高效又保持 ORM 风格。

6.3 数据本地一键备份:导出为加密 ZIP

毕设演示时,老师常问:“数据丢了怎么办?” 与其说“我有 Git”,不如现场导出一个带密码的 ZIP。

实现:用zipfile+pyminizip(支持密码)。

pip install pyminizip
# app.py 新增 /backup 路由 import pyminizip import tempfile import os @app.route('/backup') def backup_db(): # 1. 获取数据库文件绝对路径 db_path = os.path.join(app.instance_path, 'finance.db') if not os.path.exists(db_path): flash('数据库文件不存在') return redirect(url_for('index')) # 2. 创建临时 ZIP 文件 temp_dir = tempfile.mkdtemp() zip_path = os.path.join(temp_dir, 'finance_backup.zip') # 3. 用密码压缩(密码固定为 'finance2024',可改为表单输入) pyminizip.compress(db_path, "", zip_path, "finance2024", 5) # 5=压缩等级 # 4. 返回文件 return send_file( zip_path, mimetype='application/zip', as_attachment=True, download_name='finance_backup.zip' )

答辩演示动作:点击“备份”按钮 → 输入密码finance2024→ 下载 ZIP → 用 WinRAR 解压验证密码正确 → 打开finance.db用 DB Browser 确认数据完整。“老师,这是本地一键备份功能。密码加密保护隐私,ZIP 格式通用,任何电脑都能解压。它不依赖云服务,完全离线可控——这才是个人财务系统该有的安全感。”

我带毕设时,要求学生必须实现其中至少 1 项,并能讲清技术选型理由。因为真正的工程师,不是堆功能,而是用最小改动解决最大痛点。希望帮到你。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/10/28 18:00:52

Winform老项目多语言改造:XML语言包与控件树递归回填

简介&#xff1a;面向 Winform 桌面开发初学者与需要实现界面多语言切换的 C# 开发者&#xff0c;该项目围绕中英文切换场景&#xff0c;演示以 XML 文件集中存放语言文本、程序运行时动态读取并刷新控件显示的基本做法&#xff0c;开发环境标注为 Visual Studio 2012。压缩包共…

作者头像 李华
网站建设 2026/10/28 17:59:53

基于YOLO与Blob分析的工业质检智能化方案

1. 项目概述&#xff1a;工业质检的智能化升级方案在工业制造领域&#xff0c;质量检测一直是保证产品一致性和可靠性的关键环节。传统的人工检测方式不仅效率低下&#xff0c;而且容易因疲劳导致误判。我们开发的这套系统&#xff0c;通过C#上位机整合YOLO目标检测和Blob分析技…

作者头像 李华
网站建设 2026/10/28 17:59:49

Step3VL多模态大模型:视觉语言联合理解实战指南

1. 项目背景与核心价值Step3VL是阶跃星辰团队近期开源的多模态大模型项目&#xff0c;作为"清华代码熊"系列的重要成果之一&#xff0c;该项目在视觉-语言联合理解任务上展现出显著优势。我在实际测试中发现&#xff0c;其创新性的跨模态注意力机制和轻量化部署方案&…

作者头像 李华
网站建设 2026/10/28 17:59:48

MIT虫脑大模型:边缘计算AI新突破

1. MIT“虫脑”大模型技术解析最近MIT的一项突破性研究在AI圈引发震动——他们开发的“虫脑”大模型在端侧设备上实现了惊人的推理性能&#xff0c;仅需900MB内存就能流畅运行。这个开源项目一经发布就引爆开发者社区&#xff0c;因为它可能彻底改变大模型在边缘计算领域的应用…

作者头像 李华
网站建设 2026/10/28 17:58:44

从无标题到完美命名:技术项目的系统化命名方法论

1. 项目概述作为一名从业多年的技术博主&#xff0c;我经常遇到一个困扰&#xff1a;当灵感突然来临时&#xff0c;却因为各种原因无法立即为项目想出一个完美的标题。这种情况在创意工作者中非常普遍——我们可能已经有了完整的项目构思、技术方案甚至原型设计&#xff0c;却卡…

作者头像 李华
网站建设 2026/10/28 17:58:17

LangGraph、CrewAI、Coze怎么选?Agent框架选型与实战指南

1. 先搞清楚你在选什么&#xff1a;Agent框架的底层逻辑1.1 一个被问烂了但大多数人答不对的问题“LangGraph、CrewAI、Coze到底哪个好&#xff1f;”这个问题我在过去大半年里被问了不下几十次。每次有人这么问&#xff0c;我的第一反应都是反问一句&#xff1a;你要拿它干什么…

作者头像 李华