看到“基于flask的大学生记账管理系统的设计与实现-vue pycharm django”这个题目时,我第一反应是:又一个典型的课程设计/毕业设计标题。但仔细看会发现一个很有意思的现象——Flask和Django同时出现在了标题里。如果你也在做类似选题,说明你大概率还没想清楚后端框架到底用哪个,只是把听过的技术名词都堆了上去。这篇文章我不打算重讲教科书里的概念,而是以实际动手做一个记账系统为例,把从数据建模、后端接口、前端联调到Pycharm调试这一整条链路拆开讲,重点说清楚每一步为什么要这么选、这么写。
这个项目类型非常经典:用户登录、记账流水、分类管理、月度统计、可视化图表展示。功能听起来不多,但足够撑起一篇完整的课程设计或毕业设计论文。适合正在做类似选题的在校学生,也适合想通过一个完整项目把Flask和Vue串起来练手的开发者。下面我按自己实际开发时的思考顺序来写,而不是按文档目录的顺序。
1. 标题里“Flask+Django”并列,其实是选型没想清楚
1.1 记账系统为什么不需要Django的重量级能力
很多标题喜欢把Flask和Django一起写,有些是学校选题系统自动生成的“大而全”名称,有些是学生自己不确定所以都列上去了。真正动手时你必须二选一。我的建议是:这种记账管理系统,用Flask就好,不要上Django。
原因很简单。Django自带的Admin后台、ORM、迁移工具、表单系统、认证系统确实非常强大,但记账系统本身只需要用户注册登录、流水的增删改查、分类管理、统计汇总这五类核心操作。这些操作用Flask加SQLAlchemy就可以轻松完成,代码量反而更少,结构也更直白。课程设计答辩时,老师更在意你能不能说清楚每一个接口的职责和每张表的字段含义,而不是你用了多少框架特性。
Flask还有一个实际优势:启动方式直观。app.run()一跑就能看到页面,调试起来很方便。Django的manage.py命令体系虽然不复杂,但初次接触的开发者很容易在settings配置、URL路由、App结构之间绕晕。对于一个三个月左右要完成的课设来说,降低心智负担是最重要的。
但这不意味着你可以完全无视Django。如果你的项目里还有后台管理界面、用户权限分组、内容发布这类需求,Django会更快。判断标准就一条:你的核心业务是“简单CRUD加一个统计”,还是“复杂业务模型加权限体系”。前者选Flask,后者选Django。别让标题里的并列词影响你的实际技术决定。
1.2 Vue在这里的真正价值:前后端分离的可视化交互
题目里出现了Vue,说明前端要做成动态交互页面的形式。这里有一个容易踩的误区:以为用了Vue就必须做到路由级的前后端分离。其实对记账系统来说,Vue最大的价值有三个。
第一是账单列表的实时刷新。记一笔支出后,列表、月度汇总、分类占比图表要立刻同步变化,用Vue的响应式数据可以非常轻松地做到。第二是表单校验。记账页面的金额、日期、分类选择是高频操作,Vue的绑定机制让校验反馈变得即时。第三是图表渲染。配合ECharts可以做出漂亮的月度支出趋势图和分类饼图,这是答辩时最亮眼的加分项。
选择Vue 2还是Vue 3取决于你自己。如果学校教材用的是Vue 2,沿用Vue 2也不会有什么问题。如果从零开始学,我更推荐Vue 3加组合式API,组件逻辑复用会舒服很多。要注意的是,Vue本身只负责页面渲染,和后端Flask是两套完全独立的程序,它们通过HTTP接口通信。这也是前后端分离架构下你必须从一开始就习惯的思维方式。
1.3 PyCharm在整个链路里扮演的角色
标题里有PyCharm,说明开发环境大概率是PyCharm。PyCharm的社区版就能编写Flask代码,但如果你打算做前后端分离联调,建议装一下专业版中的JavaScript/Node插件,或者干脆配合VS Code打开前端目录。我的习惯是在PyCharm里写Flask后端,在VS Code里写Vue前端,两个编辑器窗口对着用。很多学生非要用一个IDE全搞定,结果前端文件和后端文件混在一起,运行逻辑乱成一团。
PyCharm真正能帮上忙的,是Flask应用的调试模式。你可以在路由函数上打断点,用Debug模式启动Flask,然后从前端页面发起请求,观察请求参数传递到了哪一行、SQLAlchemy生成了什么SQL、返回的JSON结构是否和前端预期一致。这一步比任何print大法都省时间。后面我会单独讲怎么配置同时启动前后端。
2. 数据模型先于功能:记账系统核心表怎么设计
2.1 四张表搞定基础业务:用户、账本、分类、流水
我帮某位学弟重构过一个记账项目,他上来先写页面,写了一个月发现数据总是对不上,原因就是表关系压根没设计。记账系统的基础业务,用四张表就能说清楚:用户表、账本表、分类表、流水表。
用户表负责登录和身份识别,字段包括id、用户名、密码哈希、创建时间。账本表记录“这个账本属于哪个用户”,字段是id、用户id、账本名、默认货币类型。分类表管理支出和收入的类别,如餐饮、交通、工资、兼职,字段是id、账本id、分类名、类型(收入或支出)。流水表是核心,每一条记录对应一笔收支,字段是id、账本id、分类id、金额、发生日期、备注、创建时间。
这里需要特别注意流水表和分类表的关系。很多初学者会把分类名直接字符串存进流水表,比如“餐饮”“交通”,这样乍一看没问题,但统计“本月餐饮支出多少”时,如果你改了分类名,历史数据就全错了。正确做法是流水表只保存分类id,查询时再去关联分类表取分类名。
四张表的关联关系可以这样理解:用户拥有多个账本,一个账本下有多个分类,每个分类下挂多条流水。删除用户时,级联删除该用户的所有账本、分类、流水。这个用数据库外键就能表达清楚。下面是一个简化的SQLAlchemy模型示例:
from datetime import datetime from flask_sqlalchemy import SQLAlchemy from werkzeug.security import generate_password_hash, check_password_hash db = SQLAlchemy() class User(db.Model): __tablename__ = 'user' id = db.Column(db.Integer, primary_key=True) username = db.Column(db.String(80), unique=True, nullable=False) password_hash = db.Column(db.String(200), nullable=False) created_at = db.Column(db.DateTime, default=datetime.now) def set_password(self, password): self.password_hash = generate_password_hash(password) def check_password(self, password): return check_password_hash(self.password_hash, password) class Book(db.Model): __tablename__ = 'book' id = db.Column(db.Integer, primary_key=True) user_id = db.Column(db.Integer, db.ForeignKey('user.id'), nullable=False) name = db.Column(db.String(60), nullable=False) created_at = db.Column(db.DateTime, default=datetime.now) user = db.relationship('User', backref='books') class Category(db.Model): __tablename__ = 'category' id = db.Column(db.Integer, primary_key=True) book_id = db.Column(db.Integer, db.ForeignKey('book.id'), nullable=False) name = db.Column(db.String(40), nullable=False) type = db.Column(db.String(10), nullable=False) # expense / income book = db.relationship('Book', backref='categories') class Bill(db.Model): __tablename__ = 'bill' id = db.Column(db.Integer, primary_key=True) book_id = db.Column(db.Integer, db.ForeignKey('book.id'), nullable=False) category_id = db.Column(db.Integer, db.ForeignKey('category.id'), nullable=False) amount = db.Column(db.Numeric(10, 2), nullable=False) happen_date = db.Column(db.Date, nullable=False) note = db.Column(db.String(200)) created_at = db.Column(db.DateTime, default=datetime.now) book = db.relationship('Book', backref='bills') category = db.relationship('Category', backref='bills')2.2 金额精度与时间字段:最容易给学生项目埋雷的地方
金额字段我用的是Numeric(10, 2),而不是Float。这个选择很关键。Float在Python和数据库里都存在二进制浮点误差,比如0.1加0.2可能得到0.30000000000000004。记账系统天天跟钱打交道,哪怕差一分钱,答辩演示时用计算器一核对就会很尴尬。Numeric(10, 2)表示最多10位数字,小数保留2位,查询出来的结果在SQLAlchemy里通常是Decimal对象,JSON序列化时需要转成字符串或浮点数。
关于时间字段,要区分“业务时间”和“记录时间”。业务时间就是账目实际发生的日期,用户可能补记几天前的一笔账,所以这个字段要独立成happen_date,不能直接取系统当前时间。记录时间用created_at,表示这条数据是什么时候写入系统的。很多学生只在表里放一个create_time,结果统计月度支出时,用户补记的旧账全被算到了补记那天,数据自然不对。
统计逻辑要明确:月度统计按happen_date来分组,不是按created_at。这是我在帮学弟排查项目时发现的高频问题。
2.3 数据库选型:SQLite足够应付课程设计,但要注意路径
如果不做并发量很大的真实部署,SQLite完全够用。SQLite就是一个文件数据库,Flask配好连接字符串就能用,不需要单独安装数据库服务,导出的.db文件还能直接交给导师存档。不过使用时要注意两点。
第一,数据库文件路径最好写成绝对路径或相对于项目根目录的稳定路径。有些同学把数据库文件放在临时目录里,重启电脑后路径变了,所有数据全部丢失。建议在项目根目录下建一个instance文件夹,SQLAlchemy默认会在这里放SQLite文件。
第二,SQLite对并发支持较弱,但课程设计阶段通常只有一个人在一个浏览器窗口操作,不会遇到并发问题。如果非要保证多用户同时写入,再考虑切到MySQL。切换方式很简单,改一下配置里的数据库连接字符串,模型代码基本不用动。
配置数据库连接的代码大致是这样:
import os BASE_DIR = os.path.abspath(os.path.dirname(__file__)) app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///' + os.path.join(BASE_DIR, 'instance', 'account.db') app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False3. Flask后端接口实现顺序与关键细节
3.1 从路由到蓝图:接口该怎么组织才不像一盘散沙
如果你把所有接口都堆在一个app.py文件里,短时间内是能跑的,但代码很快会膨胀到上千行。记账系统的接口可以分成三块:认证接口、账本与分类接口、流水与统计接口。每块用一个蓝图管理,文件结构清晰,答辩时也更好描述。
我喜欢后端目录按这样的形式组织:
backend/ app.py models.py extensions.py blueprints/ auth.py book_cat.py bills.py stats.py utils.pyauth.py管注册、登录、获取当前用户信息;book_cat.py管账本和分类的CRUD;bills.py管流水的新增、修改、删除、分页列表;stats.py管月度汇总、分类占比、趋势图数据。每个蓝图在app.py里用一个register_blueprint注册进来。
不要觉得蓝图是多此一举。我见过太多项目把所有接口写在同一个文件里,到后期加统计接口时,函数名撞车、变量互相覆盖,排错排到崩溃。用了蓝图之后,每个文件各管一摊,出了问题直接定位到对应蓝图层级,效率高得多。
注册蓝图的代码很简单:
from flask import Flask from extensions import db from blueprints.auth import auth_bp from blueprints.bills import bills_bp from blueprints.stats import stats_bp app = Flask(__name__) app.register_blueprint(auth_bp, url_prefix='/api/auth') app.register_blueprint(bills_bp, url_prefix='/api') app.register_blueprint(stats_bp, url_prefix='/api')3.2 登录认证选JWT的原因和落地写法
记账系统的用户登录无非两种方案:Session或JWT。传统Flask教程喜欢用Session,前端靠Cookie自动携带会话ID。但你现在是Vue前端,跨端口开发时Cookie处理会有些麻烦,而且移动端或第三方调用不便。所以更推荐JWT。
JWT的核心思路是:用户登录成功后,后端生成一个包含用户id和过期时间的加密令牌返回给前端。前端之后每次请求都在请求头里带上Authorization: Bearer <token>,后端解析这个令牌来识别用户。这种方式不依赖服务端存储会话,接口天然适合前后端分离。
在Flask中可以用PyJWT或flask-jwt-extended实现。flask-jwt-extended更省事,自带装饰器,代码写起来很简洁:
from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity @app.route('/api/auth/login', methods=['POST']) def login(): data = request.get_json() user = User.query.filter_by(username=data['username']).first() if user and user.check_password(data['password']): token = create_access_token(identity=str(user.id)) return jsonify(code=0, token=token, username=user.username) return jsonify(code=1, msg='用户名或密码错误'), 401 @app.route('/api/auth/me', methods=['GET']) @jwt_required() def me(): user_id = int(get_jwt_identity()) user = User.query.get(user_id) return jsonify(code=0, username=user.username)需要注意,JWT是有有效期的,默认一般配置几小时。如果前端在令牌过期后还继续发请求,后端会返回401。前端要能捕获401并跳回登录页,这点后面联调部分会详细说。
3.3 统计接口:用SQLAlchemy聚合查询算一个月支出
记账系统的重头戏是统计。很多同学一拿到需求就去写一堆循环,从数据库把所有流水捞出来然后在Python里加减乘除。这种写法在小数据量下没问题,但更专业的做法是把聚合逻辑写进SQL查询里。SQLAlchemy对这种情况有很好的支持。
举个例子,统计当前用户某个月按分类汇总的支出金额:
from sqlalchemy import func from datetime import datetime month_start = datetime(year, month, 1).date() if month == 12: next_month_start = datetime(year + 1, 1, 1).date() else: next_month_start = datetime(year, month + 1, 1).date() results = ( db.session.query(Category.name, func.sum(Bill.amount)) .join(Bill, Bill.category_id == Category.id) .filter(Bill.book_id == current_book_id) .filter(Bill.happen_date >= month_start) .filter(Bill.happen_date < next_month_start) .filter(Category.type == 'expense') .group_by(Category.id) .all() )这一段返回的就是分类名和金额的列表。要注意SQLAlchemy返回的金额可能是Decimal,做JSON序列化时需要转成字符串或者用float()处理。
统计月度总支出时,直接把上面查询里的分类分组去掉,改成func.sum(Bill.amount)就行。趋势图数据可以用类似方式按天分组,得到这个月每天的支出合计。把这些数据组合成一个接口,前端拿到之后喂给ECharts,折线图和饼图立刻就能渲染出来。
4. Vue前端连接Flask的完整链路与踩坑记录
4.1 前端分层与组件设计
Vue前端如果只做一个页面所有功能堆在一起,后期改需求会非常痛苦。我建议至少把components分成三个层级:页面容器、业务组件、通用组件。
页面容器负责布局和数据请求,比如Dashboard.vue、BillList.vue、Login.vue。业务组件是页面里的一个功能块,比如BillForm.vue负责新增和编辑账单的表单,CategoryManager.vue负责管理分类,MonthlyChart.vue负责渲染统计图表。通用组件则是按钮、弹窗、日期选择器等可复用元素。
记账系统的核心交互场景是:用户打开账单列表,点击新增,弹出表单,选择分类和金额,保存后列表和统计图表同步刷新。这个场景非常适合用组件化实现。父组件通过props传数据给子组件,子组件通过$emit或emit通知父组件刷新。在Vue 3里用组合式API写起来也不复杂。
前端请求后端的接口设计要保持统一风格。我用过两种风格,一种是成功返回code: 0加data,另一种是直接依赖HTTP状态码。在项目里我更偏向前者:后端所有接口统一返回JSON结构,业务错误也返回HTTP 200,前端只需要判断code字段。这样联调时非常直观,前端和后端各有一套约定,不容易扯皮。
4.2 Axios封装和跨域处理
Vue项目通过Axios发HTTP请求,每个页面都直接axios.get虽然能用,但重复代码太多。我在接手这类项目时,第一步永远是封装一个统一的Axios实例。封装要做三件事:设置基础URL、添加请求拦截器自动携带token、添加响应拦截器统一处理业务错误和401跳转。
基础URL在开发阶段通常是一长串,比如http://127.0.0.1:5000/api。每个组件都写一遍后续改端口时就要全局搜索替换,不如集中配置:
// request.js import axios from 'axios' import { ElMessage } from 'element-plus' import router from '../router' const request = axios.create({ baseURL: '/api', timeout: 10000 }) request.interceptors.request.use( config => { const token = localStorage.getItem('token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }, error => Promise.reject(error) ) request.interceptors.response.use( response => { const res = response.data if (res.code === 0) { return res } ElMessage.error(res.msg || '请求失败') return Promise.reject(new Error(res.msg)) }, error => { if (error.response && error.response.status === 401) { localStorage.removeItem('token') router.push('/login') } ElMessage.error('网络错误,请稍后重试') return Promise.reject(error) } ) export default request跨域问题也是新手经常被卡住的地方。开发时Vue跑在8080端口,Flask跑在5000端口,前后端端口不同,浏览器默认会拦截非同源的请求。Flask后端需要启用CORS支持。最简单的做法是安装flask-cors,然后在创建应用时加上:
from flask_cors import CORS CORS(app, supports_credentials=True, origins=['http://localhost:8080'])这样开发时前端就能正常请求了。部署到真实服务器时,可以把前端的静态构建文件交给Flask托管,或者用Nginx做反向代理,端口不一致的问题就迎刃而解。
4.3 我踩过的两个前端联调坑:日期格式和token过期
联调中最隐蔽的一个坑是日期格式。前端表单选择的日期一般是2025-06-15这种字符串,而后端Bill.happen_date字段是Date类型。如果直接用字符串去查询或保存,SQLAlchemy可能会报错或产生时区偏差。可靠做法是前端统一发送YYYY-MM-DD字符串,后端用datetime.strptime解析成Date对象。在统计接口里,前端传给后端的月份参数也最好用2025-06这种格式,后端再拼接成月初和月末。
还有token过期。很多项目只在登录时存了token,完全没处理过期场景。结果用户用着用着,所有请求突然全部失败,且没有任何提示,控制台全是401。我在上面的Axios响应拦截器里专门处理了401:清除本地token,跳转登录页。这个细节值得反复提醒,因为答辩演示的时候,前端某个接口报401会非常尴尬。
把这两个点解决了,前后端联调的过程会顺畅很多。我在实际项目里还遇到过ECharts数据更新不刷新的问题:图表已经拿到了新数据,但DOM没有重新渲染。解决办法是给图表组件绑定一个:key,切换月份或刷新数据时强制重新创建组件。
5. PyCharm运行调试与答辩演示的实战经验
5.1 后端前端同时跑的启动配置
很多同学喜欢开两个终端窗口手动启动,其实PyCharm里可以配置两个配置文件。
先把前端项目在终端中执行npm install和npm run serve,确保Vue跑起来。然后在PyCharm中新建一个Python运行配置,选择app.py作为脚本路径,添加上环境变量FLASK_ENV=development,运行端口固定为5000。这样在PyCharm里点一下“运行”按钮就能启动Flask后端,再点一下浏览器访问http://localhost:8080,两个服务就同时在线了。
有一个容易忽略的地方:Vue的devServer默认会代理/api请求到后端。如果配置不对,即使CORS开了,也可能出现请求404。建议在Vue项目的vue.config.js里配置代理:
module.exports = { devServer: { port: 8080, proxy: { '/api': { target: 'http://127.0.0.1:5000', changeOrigin: true } } } }这样前端请求/api/auth/login会代理到Flask的http://127.0.0.1:5000/api/auth/login,而且不会存在跨域问题,开发体验更好。
5.2 初始化演示数据脚本
答辩前最怕的就是数据库里没有数据,页面空空荡荡。一定要准备一个初始化脚本,一键生成测试账号和几十条模拟流水。
初始化脚本可以单独放在scripts/init_data.py里,逻辑大致是:创建一个测试用户,创建一个账本,创建餐饮、交通、娱乐、工资等分类,然后循环生成过去三个月的随机流水。金额、日期、分类都随机化,保证图表有数据可展示。
实际写的时候要注意金额随机生成后保留两位小数,日期不要生成未来时间。最好让每月支出呈现一定的波动,这样趋势图看起来更自然,不会是一条直线。答辩演示时,我一般先用测试账号登录,展示一下月度总支出、分类饼图、近七日趋势,然后现场新增一笔支出,让列表和图表同时刷新,这个动作特别能体现系统“活”的一面。
5.3 答辩演示现场最容易翻车的三个点
第一个翻车点是数据库路径不一致。有些同学在自己电脑上运行正常,换到演示机器上后数据库文件没拷贝过去,结果页面一堆空数据。建议把初始化的.db文件放在项目目录下,并把它纳入提交产物,演示前先跑一次初始化脚本确认能登录。
第二个翻车点是前端依赖没装全。演示前不要光在后端运行调试,一定要先确认npm install成功且npm run serve能起来。有条件的话,最好提前把Vue项目构建成静态文件,由Flask统一托管,这样只需要一个Flask服务在跑,不需要在演示时打开Node服务。
第三个翻车点是网络端口被占用。某个端口被残留进程占用很常见,尤其是5000端口和8080端口。演示前先检查一下两个端口是否空闲,或者在配置里换掉默认端口。
我在做这类项目时还有一个个人习惯:在演示环境里准备一份接口调试列表,把所有接口和对应的JSON样例打印出来放在手边。万一前端页面出了BUG,可以直接用浏览器访问后端接口验证数据是否正确,这样能快速判断问题出在前端还是后端。
如果时间允许,建议在这个项目基础上再加一个Excel导出功能,把账单流水导出成表格。这个功能在教务检查、导师审阅时非常实用,而且实现起来不算难,只需要后端返回一个文件流,前端用Blob接收即可。它能让你的项目在功能完整性上明显高出平均水平。