admin 管理员组

文章数量: 1184232

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

简介:本文介绍了一个由个人开发的学生财务管理系统,旨在帮助学生群体记录收支、进行预算规划和消费分析,培养良好的理财习惯。系统具备收支记录、分类统计、预算设定、报表生成及数据备份恢复等核心功能,采用HTML/CSS/JavaScript前端技术,结合Flask/Django或Express后端框架,使用MySQL或SQLite数据库存储数据,并通过Git进行版本控制。该项目不仅实用性强,还涵盖了前端、后端、数据库和软件工程流程,是初学者学习全栈开发的理想实践项目。

学生财务管理系统:从零构建一个实用又优雅的全栈应用 🎯

你有没有过这样的经历?刚开学时雄心勃勃地立下flag:“这个月只花500块吃饭!”结果月底一看账单,奶茶+外卖+聚餐……直接飙到1200。🤯 而且还不知道钱到底去哪儿了。

高校学生的消费场景越来越丰富——食堂、外卖、网购、共享单车、网课会员……每一笔都是“小支出”,合起来却是个“大窟窿”。传统的记账本早就跟不上节奏,Excel表格也显得太笨重。我们真正需要的,是一款 轻量、直观、智能 的财务管理工具。

今天,我们就来一起打造这样一个系统:它不炫技,但够用;不复杂,但专业。前端用 Vue 构建响应式界面,后端用 Flask 提供 RESTful API,数据存在 SQLite 里,本地跑得飞快,部署也简单。整个项目采用 Git 进行版本控制,代码清晰可维护,未来还能轻松扩展成多用户云同步版本。

这不仅是一个学生理财助手,更是一份 可复用的全栈开发蓝图 。无论你是想练手的小白,还是正在带课的老师,都能从中获得启发。


收支记录:系统的数据心脏 💓

所有功能都建立在一条条收支记录之上。没有准确的数据录入,后续的统计、预算、图表全都成了空中楼阁。所以,我们要做的第一件事,就是设计一个既能反映现实、又能支撑未来的数据模型。

数据模型不是字段堆砌,而是对现实的抽象 🧠

先别急着写代码。我们得问自己:一笔“交易”到底包含哪些信息?

对于学生来说,消费有几个特点:
- 小额高频 :一天可能有三四笔支出;
- 类别集中 :餐饮、交通、学习资料是三大主力;
- 时间敏感 :周末花钱多,月初紧巴巴,月底靠“吃土”。

因此,我们的数据结构必须能捕捉这些行为特征,同时为未来的分析留出空间。

核心字段怎么定?看这张表 👇
字段名 类型 是否必填 说明
id INTEGER (PK) 唯一标识,自增
user_id INTEGER 用户ID(预留多用户支持)
amount DECIMAL(10,2) 金额,正负号区分收支
type TEXT (‘income’/’expense’) 明确类型,避免歧义
category VARCHAR(50) 消费分类,如“餐饮”、“交通”
date DATE 交易日期
time TIME 精确到时间点(可选)
description TEXT 备注,比如“图书馆打印5页”
created_at DATETIME 创建时间,用于审计
updated_at DATETIME 最后修改时间

⚠️ 注意一个小细节:为什么用 amount 统一表示,并通过正负区分收入和支出?而不是拆成两个字段?

因为这样更简洁!想象你要做“月度总流水”,如果拆开就得分别求和再合并;而统一字段可以直接 SUM(amount) ,负数自动抵消。而且数据库层面可以用 CHECK(amount != 0) 防止无效记录。

下面是建表语句,已经加入了完整的约束:

CREATE TABLE transactions (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id INTEGER NOT NULL DEFAULT 1,
    amount DECIMAL(10,2) NOT NULL CHECK(amount != 0),
    type TEXT NOT NULL CHECK(type IN ('income', 'expense')),
    category VARCHAR(50) NOT NULL,
    date DATE NOT NULL,
    time TIME,
    description TEXT,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    updated_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id)
);

是不是看起来就很稳?😉 每个字段都有意义,每个约束都有作用。

四层防护体系:让脏数据无处遁形 🛡️

用户输入永远是最不可信的。你不信?试试让朋友填一次你的记账App,保证有人会输“abc”当金额,或者选“未来”的日期。

所以我们不能只靠前端提醒,得有一套纵深防御机制。我把它叫做“四层防护体系”:

graph TD
    A[前端输入层] --> B[接口校验层]
    B --> C[数据库约束层]
    C --> D[业务逻辑层]
    style A fill:#FFE4B5,stroke:#333
    style B fill:#98FB98,stroke:#333
    style C fill:#87CEEB,stroke:#333
    style D fill:#DDA0DD,stroke:#333
    subgraph "数据写入流程"
        A -->|实时验证| B
        B -->|请求过滤| C
        C -->|触发钩子| D
    end
  • 前端输入层(黄色) :第一时间反馈错误。比如金额框只能输入数字,日期选择器禁用未来日期。
  • 接口校验层(绿色) :服务端收到请求后,先检查参数是否完整、格式是否正确。
  • 数据库约束层(蓝色) :最后一道硬防线,任何绕过API的操作都会被拦住。
  • 业务逻辑层(紫色) :处理高级规则,比如“同一笔交易不能重复提交”。

举个例子:有人想录一笔“0元支出”。

  • 前端看到金额为空就标红提示;
  • 如果他用 Postman 强行发请求,Flask 接口解析 JSON 时发现缺少字段,返回 400;
  • 即便参数齐全但值为0,数据库的 CHECK(amount != 0) 也会拒绝插入;
  • 如果程序逻辑出错漏过了前面几层,业务层还可以再补一刀。

这种“层层设卡”的策略,大大降低了系统崩溃的风险。

具体一致性规则怎么落地?
规则名称 描述 实施位置 技术手段
非零金额约束 金额不能为0 数据库层 CHECK(amount != 0)
类型-金额符号一致 收入必须 >0,支出必须 <0 业务逻辑层 条件判断 + 异常抛出
时间顺序合理 不允许录入超过当前时间的交易 前端+接口层 JS Date 对比 / Python datetime 检查
分类白名单控制 只允许选择预设分类 前端+接口层 下拉菜单限制 + 枚举校验
单日重复条目防重 防止相同内容短时间内重复提交 接口层 请求指纹去重(基于 content hash)

特别是最后一个“防重”机制,在移动端尤其重要。试想一下,网络卡顿导致按钮连点两次,结果同一条支出被记了两遍……那还不得疯掉?

我们可以这样做:

import hashlib

def generate_request_fingerprint(data):
    # 把关键字段拼起来做哈希
    key_string = f"{data['amount']}_{data['category']}_{data['date']}"
    return hashlib.md5(key_string.encode()).hexdigest()

然后缓存最近几分钟内的指纹,发现重复就直接拦截。


后端 API:连接前后端的生命线 🔗

API 是现代 Web 应用的核心。它就像快递员,把前端的请求送到后端,再把处理结果打包送回来。在 Flask 中,我们可以用极简的方式实现一套健壮的 RESTful 接口。

用 Flask 写 API,真的可以很清爽 ✨

from flask import Flask, request, jsonify
from datetime import datetime
import sqlite3

app = Flask(__name__)

def get_db_connection():
    conn = sqlite3.connect('finance.db')
    conn.row_factory = sqlite3.Row  # 查询结果像字典一样访问
    return conn

# 获取所有记录
@app.route('/api/transactions', methods=['GET'])
def get_transactions():
    conn = get_db_connection()
    cursor = conn.cursor()
    cursor.execute("SELECT * FROM transactions ORDER BY date DESC, created_at DESC")
    rows = cursor.fetchall()
    conn.close()

    result = [dict(row) for row in rows]
    return jsonify(result), 200

# 添加新记录
@app.route('/api/transactions', methods=['POST'])
def add_transaction():
    data = request.get_json()
    required_fields = ['amount', 'type', 'category', 'date']
    if not all(field in data for field in required_fields):
        return jsonify({'error': '缺少必要字段'}), 400

    # 校验金额与类型匹配
    if (data['type'] == 'income' and data['amount'] < 0) or \
       (data['type'] == 'expense' and data['amount'] > 0):
        return jsonify({'error': '金额与类型不匹配'}), 400

    conn = get_db_connection()
    try:
        cursor = conn.cursor()
        cursor.execute("""
            INSERT INTO transactions (user_id, amount, type, category, date, time, description)
            VALUES (?, ?, ?, ?, ?, ?, ?)
        """, (
            data.get('user_id', 1),
            data['amount'],
            data['type'],
            data['category'],
            data['date'],
            data.get('time'),
            data.get('description')
        ))
        connmit()
        new_id = cursor.lastrowid
        return jsonify({'id': new_id, 'message': '记录添加成功'}), 201
    except sqlite3.IntegrityError as e:
        conn.rollback()
        return jsonify({'error': '数据库约束冲突: ' + str(e)}), 400
    finally:
        conn.close()

这段代码虽然短,但五脏俱全:
- 使用参数化查询防止 SQL 注入;
- 手动管理事务,出错自动回滚;
- 返回标准 HTTP 状态码(200/201/400);
- 错误信息清晰可读,方便调试。

最关键的是: 它足够简单,新手也能看懂;但它又足够严谨,生产环境也能跑

参数校验太麻烦?交给 jsonschema 吧 🤖

随着接口变多,手动写校验逻辑容易重复。这时候可以用 jsonschema 实现声明式验证:

from jsonschema import validate, ValidationError

TRANSACTION_SCHEMA = {
    "type": "object",
    "properties": {
        "amount": {"type": "number", "exclusiveMinimum": -10000, "exclusiveMaximum": 10000},
        "type": {"enum": ["income", "expense"]},
        "category": {"type": "string", "minLength": 1, "maxLength": 50},
        "date": {"type": "string", "format": "date"},
        "time": {"type": "string", "format": "time", "nullable": True},
        "description": {"type": "string", "maxLength": 200}
    },
    "required": ["amount", "type", "category", "date"]
}

def validate_request(data):
    try:
        validate(instance=data, schema=TRANSACTION_SCHEMA)
        return None
    except ValidationError as e:
        return e.message

之后在每个 POST 路由开头加一句:

error = validate_request(data)
if error:
    return jsonify({'error': error}), 400

干净利落,一劳永逸。

全局异常处理:给 API 加一层温柔的壳 🐚

谁都不希望看到服务器报 500 还返回一堆 HTML 错误页。我们应该统一响应格式,哪怕出错了也要体面。

@app.errorhandler(404)
def not_found(e):
    return jsonify({'error': '接口不存在'}), 404

@app.errorhandler(500)
def internal_error(e):
    return jsonify({'error': '服务器内部错误'}), 500

class InvalidUsage(Exception):
    status_code = 400
    def __init__(self, message, status_code=None):
        super().__init__()
        self.message = message
        if status_code is not None:
            self.status_code = status_code

@app.errorhandler(InvalidUsage)
def handle_invalid_usage(error):
    return jsonify({'error': error.message}), error.status_code

现在你可以放心地在业务逻辑中抛出异常了:

if is_over_budget(...):
    raise InvalidUsage("这笔支出将导致超支,请确认是否继续")

前端拿到的永远是结构化的 JSON,不会乱套。


消费分类统计:让数据说话 📊

记账的目的不是为了记账,而是为了看清自己的消费模式。这就需要强大的统计能力。

分类体系怎么设计才不翻车?🧠

很多记账App失败的原因是分类太乱。要么太粗,“其他”占一半;要么太细,“早餐-豆浆油条”和“早餐-包子鸡蛋”分开算……

我们要走中间路线: 一级分类清晰,二级分类灵活

经过调研,学生主要消费集中在以下几类:

类别 典型子项 心理动因 是否高频
餐饮 外卖、奶茶、食堂 生理需求 是 ✅
交通 地铁、单车、打车 移动自由 是 ✅
学习 教材、网课、考试报名 自我提升 中 ⚠️
娱乐 游戏充值、电影票 放松社交 否 ❌
生活用品 衣服、洗护 日常维护 中 ⚠️
医疗健康 药品、体检 安全保障 否 ❌
其他 不明小额支出 冲动消费 视情况

初始化时插入默认分类:

INSERT INTO categories (name, type, icon, color, is_default) VALUES
('餐饮', 'expense', 'fas fa-utensils', '#FF6B6B', 1),
('交通', 'expense', 'fas fa-bus', '#4ECDC4', 1),
('学习', 'expense', 'fas fa-book', '#45B7D1', 1),
('娱乐', 'expense', 'fas fa-gamepad', '#96CEB4', 1),
('生活用品', 'expense', 'fas fa-shopping-bag', '#FFEAA7', 1),
('医疗健康', 'expense', 'fas fa-heartbeat', '#DDA0DD', 1),
('其他', 'expense', 'fas fa-question-circle', '#CCCCCC', 1);

图标用 Font Awesome,颜色统一风格,用户体验瞬间提升一大截!

多层级分类:支持“钻取”分析 🔍

有些场景需要更深的粒度。比如“学习”下面还想分“书籍”、“课程”、“考试”。

这时候就要上树形结构了:

CREATE TABLE categories (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    name TEXT NOT NULL UNIQUE,
    type TEXT CHECK(type IN ('expense', 'income')) DEFAULT 'expense',
    parent_id INTEGER,
    icon TEXT,
    color TEXT,
    is_default BOOLEAN DEFAULT TRUE,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (parent_id) REFERENCES categories(id)
);

配合递归查询(SQLite 支持 CTE):

WITH RECURSIVE category_tree AS (
    SELECT id, name, parent_id FROM categories WHERE id = 3
    UNION ALL
    SELECT c.id, c.name, c.parent_id
    FROM categories c
    JOIN category_tree ct ON c.parent_id = ct.id
)
SELECT * FROM category_tree;

前端点击“学习”,就能展开看到“书籍:¥400”、“课程:¥300”……

是不是有种“数据分析师”的感觉了?😎

后端聚合查询要快,更要准 ⚡

统计的本质是聚合。SQL 的 GROUP BY SUM() 就是为此而生。

按类别汇总支出:
SELECT 
    c.name AS category,
    SUM(t.amount) AS total_expense
FROM transactions t
JOIN categories c ON t.category_id = c.id
WHERE t.user_id = ?
  AND t.type = 'expense'
  AND t.date BETWEEN ? AND ?
GROUP BY c.name
ORDER BY total_expense DESC;

记得加索引哦:

CREATE INDEX idx_transactions_user_date ON transactions(user_id, date);
CREATE INDEX idx_transactions_category_date ON transactions(category_id, date);

万条数据也能毫秒级返回。

按月份看趋势:
SELECT 
    strftime('%Y-%m', t.date) AS month,
    SUM(t.amount) AS monthly_total
FROM transactions t
WHERE t.user_id = ?
  AND t.type = 'expense'
  AND t.date >= date('now', '-6 months')
GROUP BY month
ORDER BY month ASC;

这个结果拿去做折线图,消费波动一目了然。


前端可视化:把数字变成故事 🎨

ECharts 上场的时候到了!

安装很简单:

npm install echarts --save

Vue 中初始化饼图:

import * as echarts from 'echarts';

export default {
  mounted() {
    this.chart = echarts.init(this.$refs.chartContainer);
    this.updateChart();
  },
  methods: {
    async updateChart() {
      const response = await fetch('/api/stats/category?user_id=1&start=2024-01-01&end=2024-01-31');
      const data = await response.json();

      const option = {
        title: { text: '本月消费分布' },
        tooltip: { trigger: 'item' },
        legend: { top: '5%', left: 'center' },
        series: [{
          name: '金额',
          type: 'pie',
          radius: ['40%', '70%'],
          avoidLabelOverlap: false,
          itemStyle: {
            borderRadius: 10,
            borderColor: '#fff',
            borderWidth: 2
          },
          label: { show: true },
          emphasis: { label: { show: true, fontSize: 16 } },
          data: data.data.map(item => ({
            name: item.category,
            value: item.amount
          }))
        }]
      };

      this.chart.setOption(option);
    }
  }
}

再加上响应式布局:

.chart-container {
  width: 100%;
  height: 400px;
  margin: 20px 0;
}

@media (max-width: 768px) {
  .chart-container {
    height: 300px;
    font-size: 12px;
  }
}

window.addEventListener('resize', () => {
  this.chart?.resize();
});

搞定!📱💻 不管手机还是电脑,图表都美美的。


预算与提醒:真正的“财务教练” 🚨

记账只是第一步, 控制支出才是目标

预算模型怎么做才科学?

两种模式:
- 固定周期 :每月自动重置,适合常规开支;
- 自定义周期 :比如“国庆旅行周”,限定7天花500元。

表结构长这样:

CREATE TABLE budgets (
    id INTEGER PRIMARY KEY AUTOINCREMENT,
    user_id INTEGER NOT NULL,
    category TEXT NOT NULL,
    amount_limit REAL NOT NULL,
    cycle_type TEXT CHECK(cycle_type IN ('monthly', 'custom')) DEFAULT 'monthly',
    start_date DATE,
    end_date DATE,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY(user_id) REFERENCES users(id),
    UNIQUE(user_id, category, start_date, end_date)
);

实时超支检测:在错误发生前喊停 🔔

每次新增支出,先问问:“这会不会超预算?”

def is_over_budget(category, amount, current_date):
    budget = db.query(Budget).filter(
        Budget.category == category,
        Budget.user_id == current_user.id,
        Budget.start_date <= current_date,
        Budget.end_date >= current_date
    ).first()

    if not budget:
        return False

    spent = db.query(func.sum(Transaction.amount)).filter(
        Transaction.category == category,
        Transaction.user_id == current_user.id,
        Transaction.date >= budget.start_date,
        Transaction.date <= current_date
    ).scalar() or 0.0

    return (spent + amount) > budget.amount_limit

在创建路由中调用:

if is_over_budget(...):
    return jsonify({
        'warning': f"此项支出将导致【{category}】超支",
        'can_proceed': True
    }), 206

前端收到 206 状态码,弹窗确认:“确定要超支吗?” 💬

智能推荐:帮新手迈出第一步 🤝

第一次设置预算总觉得难?那就给建议值!

def recommend_budget(category, user_id):
    avg_monthly = db.query(func.avg(Transaction.amount)).filter(
        Transaction.category == category,
        Transaction.user_id == user_id,
        Transaction.date >= func.date('now', '-3 month')
    ).group_by(func.strftime('%Y-%m', Transaction.date)).all()

    if avg_monthly:
        recommended = sum([r[0] for r in avg_monthly]) / len(avg_monthly)
        return round(recommended * 1.2, -1)  # 上浮20%,取整十
    else:
        return DEFAULT_BUDGETS.get(category, 100)

新人打开App,看到“系统建议:餐饮 ¥600/月”,心里踏实多了。


报表导出与系统维护:走得更远的底气 🛠️

导出 Excel?用 xlsxwriter 超简单

import xlsxwriter

def export_monthly_report(year, month):
    filename = f"report_{year}_{month}.xlsx"
    workbook = xlsxwriter.Workbook(filename)
    sheet = workbook.add_worksheet("收支明细")

    bold = workbook.add_format({'bold': True})
    money = workbook.add_format({'num_format': '#,##0.00'})

    headers = ["日期", "类别", "金额", "描述"]
    for col, h in enumerate(headers):
        sheet.write(0, col, h, bold)

    expenses = get_monthly_expenses(year, month)
    for row, exp in enumerate(expenses, start=1):
        sheet.write(row, 0, exp.date.strftime('%m-%d'))
        sheet.write(row, 1, exp.category)
        sheet.write(row, 2, exp.amount, money)
        sheet.write(row, 3, exp.description)

    sheet.set_column('A:A', 10)
    sheet.set_column('B:B', 12)
    sheet.set_column('C:C', 10)
    sheet.set_column('D:D', 25)

    workbook.close()
    return filename

前端一个链接搞定下载:

<a href="/export/excel?month=2024-09" download>📥 导出本月报表</a>

数据备份不能少

定时任务每天凌晨备份一次:

# crontab entry
0 2 * * * sqlite3 /data/student_finance.db ".backup '/backup/db_$(date +\%Y\%m\%d).db'"

万一出问题,手动恢复也方便:

@app.route('/admin/restore', methods=['POST'])
def restore_backup():
    file = request.files['backup']
    filepath = os.path.join(BACKUP_DIR, file.filename)
    file.save(filepath)

    with app.app_context():
        db.engine.dispose()
    os.replace(filepath, DATABASE_PATH)
    return jsonify({"status": "success"})

Git 版本控制:团队协作的基石 🌿

用 Git Flow 管理迭代:

graph LR
    main --> release/v1.2
    develop --> feature/budget-alert
    feature/budget-alert --> develop
    release/v1.2 --> hotfix/email-fix
    hotfix/email-fix --> main
    hotfix/email-fix --> release/v1.2

关键实践:
- 功能分支命名: feature/{module}-{brief}
- 提交遵循 Conventional Commits
- .gitignore 排除敏感文件
- GitHub Actions 自动测试部署

每个版本打 tag,写 CHANGELOG,谁改了啥,清清楚楚。


结语:这不是终点,而是起点 🌱

我们完成的不仅仅是一个记账App,而是一个 可成长的系统骨架

今天它是单机版,明天可以加上用户登录、云同步;
现在只有基础统计,未来可以加入AI消费分析、趋势预测;
目前是个人使用,将来能拓展到学生社团经费管理。

技术栈轻巧但不廉价,架构清晰且易于维护。它适合教学演示,也足以投入真实使用。

最重要的是——它真的能帮你管住钱包。💪

所以,别再让每一分钱都悄无声息地溜走。
从现在开始,让你的钱,看得见、算得清、管得住。

“财富自由的第一步,不是赚更多钱,而是知道自己花了多少钱。” —— 一位记账三年的学生说 🙋‍♂️

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

简介:本文介绍了一个由个人开发的学生财务管理系统,旨在帮助学生群体记录收支、进行预算规划和消费分析,培养良好的理财习惯。系统具备收支记录、分类统计、预算设定、报表生成及数据备份恢复等核心功能,采用HTML/CSS/JavaScript前端技术,结合Flask/Django或Express后端框架,使用MySQL或SQLite数据库存储数据,并通过Git进行版本控制。该项目不仅实用性强,还涵盖了前端、后端、数据库和软件工程流程,是初学者学习全栈开发的理想实践项目。


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

本文标签: 详解 实战 财务管理系统 学生