在大学校园里,记账管理是许多学生头疼的问题。纸质记账容易丢失,Excel表格又缺乏分类统计功能。作为一个长期被这个问题困扰的开发者,我决定用Flask+Vue.js技术栈打造一个轻量级的大学生记账系统。这个系统不仅能解决基础记账需求,还能通过可视化图表帮助学生分析消费结构。
选择Flask作为后端框架主要基于三点考虑:首先,它的轻量级特性特别适合学生这类小型项目快速迭代;其次,Python语法简洁,适合非计算机专业学生学习;最后,Flask的扩展机制可以按需添加功能,不会造成资源浪费。对比Django的全家桶模式,Flask的灵活性在这个场景下更具优势。
前端选用Vue.js 2.x版本而非React,是因为其渐进式特性更符合项目需求。学生开发者可以先用基础功能快速上手,后续再逐步引入Vuex状态管理。Element-UI组件库的选择则大幅提升了开发效率,其表单验证、表格展示等组件开箱即用。
开发工具方面,PyCharm Professional版提供了完整的Flask开发支持,包括:
系统采用经典的前后端分离架构:
code复制前端(Vue.js) ← HTTP API → 后端(Flask)
↑
数据库(MySQL)
这种架构的优势在于:
采用JWT(JSON Web Token)实现无状态认证,相比传统的Session机制更符合RESTful规范。具体实现时需要注意:
密码存储采用PBKDF2算法加盐哈希,即使数据库泄露也不会暴露明文密码。Flask中可以通过Werkzeug库轻松实现:
python复制from werkzeug.security import generate_password_hash, check_password_hash
# 注册时存储哈希值
user.password_hash = generate_password_hash(password)
# 登录时验证
if check_password_hash(user.password_hash, input_password):
# 验证通过
这是系统的核心功能,数据库设计需要考虑学生记账的特点:
对应的Transaction模型包含以下关键字段:
python复制class Transaction(db.Model):
TYPE_INCOME = 'income'
TYPE_EXPENSE = 'expense'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('user.id'))
amount = db.Column(db.Float, nullable=False)
category = db.Column(db.String(50), index=True) # 添加索引提高查询效率
type = db.Column(db.String(10)) # income/expense
note = db.Column(db.Text) # 备注信息
timestamp = db.Column(db.DateTime, index=True, default=datetime.utcnow)
使用ECharts实现三种可视化图表:
后端API设计特别注意了数据聚合效率:
python复制@app.route('/api/stats/monthly')
@jwt_required
def monthly_stats():
# 获取当前用户最近6个月的统计数据
result = db.session.query(
func.strftime('%Y-%m', Transaction.timestamp).label('month'),
func.sum(case([(Transaction.type == 'income', Transaction.amount)], else_=0)).label('income'),
func.sum(case([(Transaction.type == 'expense', Transaction.amount)], else_=0)).label('expense')
).filter(
Transaction.user_id == get_jwt_identity(),
Transaction.timestamp >= datetime.now() - timedelta(days=180)
).group_by('month').order_by('month')
return jsonify([dict(row) for row in result])
推荐使用pyenv管理多版本Python环境:
bash复制# 安装Python 3.8.12
pyenv install 3.8.12
# 创建项目专属虚拟环境
pyenv virtualenv 3.8.12 finance-env
# 激活环境
pyenv activate finance-env
关键依赖安装:
bash复制pip install flask flask-sqlalchemy flask-migrate flask-jwt-extended flask-cors
使用Vue CLI 4.x创建项目:
bash复制npm install -g @vue/cli
vue create finance-frontend
选择手动配置:
安装UI库和图表库:
bash复制cd finance-frontend
npm install element-ui echarts axios
打开后端项目目录
设置Python解释器为之前创建的虚拟环境
配置Flask运行配置:
venv/bin/flaskrun --port=5000FLASK_APP=app.py FLASK_ENV=development启用Live Template功能,添加Flask路由模板:
python复制@app.route('$PATH$', methods=['$METHOD$'])
def $NAME$():
$END$
虽然SQLite适合开发阶段,但考虑到学生可能需要进行小组协作开发,最终选择MySQL 5.7作为生产数据库,原因包括:
使用Flask-SQLAlchemy配置数据库连接:
python复制app.config['SQLALCHEMY_DATABASE_URI'] = 'mysql+pymysql://user:password@localhost/finance'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
python复制class User(db.Model):
__tablename__ = 'users'
id = db.Column(db.Integer, primary_key=True)
username = db.Column(db.String(80), unique=True, nullable=False)
email = db.Column(db.String(120), unique=True, nullable=False)
password_hash = db.Column(db.String(128))
created_at = db.Column(db.DateTime, default=db.func.now())
transactions = db.relationship('Transaction', backref='user', lazy='dynamic')
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)
python复制class Transaction(db.Model):
__tablename__ = 'transactions'
id = db.Column(db.Integer, primary_key=True)
user_id = db.Column(db.Integer, db.ForeignKey('users.id'), nullable=False)
amount = db.Column(db.Float, nullable=False)
category = db.Column(db.String(50), index=True)
type = db.Column(db.String(10), nullable=False) # income/expense
note = db.Column(db.Text)
location = db.Column(db.String(100)) # 消费地点
timestamp = db.Column(db.DateTime, index=True, default=db.func.now())
__table_args__ = (
db.Index('idx_user_timestamp', 'user_id', 'timestamp'), # 复合索引
)
使用Flask-Migrate管理数据库变更:
bash复制flask db init # 初始化迁移目录
flask db migrate -m "initial migration" # 生成迁移脚本
flask db upgrade # 应用迁移
重要提示:开发过程中每次修改模型后都需要执行migrate和upgrade命令,确保数据库结构与模型保持一致。
遵循以下设计原则:
python复制from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity
@app.route('/api/auth/login', methods=['POST'])
def login():
username = request.json.get('username')
password = request.json.get('password')
user = User.query.filter_by(username=username).first()
if not user or not user.check_password(password):
return jsonify({"msg": "Bad credentials"}), 401
access_token = create_access_token(identity=user.id)
return jsonify(access_token=access_token)
@app.route('/api/protected', methods=['GET'])
@jwt_required
def protected():
current_user = get_jwt_identity()
return jsonify(logged_in_as=current_user), 200
python复制@app.route('/api/transactions', methods=['GET'])
@jwt_required
def get_transactions():
page = request.args.get('page', 1, type=int)
per_page = request.args.get('per_page', 10, type=int)
query = Transaction.query.filter_by(user_id=get_jwt_identity())
# 支持按类型、分类、时间范围筛选
if 'type' in request.args:
query = query.filter_by(type=request.args['type'])
if 'category' in request.args:
query = query.filter_by(category=request.args['category'])
if 'start_date' in request.args:
query = query.filter(Transaction.timestamp >= request.args['start_date'])
if 'end_date' in request.args:
query = query.filter(Transaction.timestamp <= request.args['end_date'])
pagination = query.order_by(Transaction.timestamp.desc()).paginate(
page, per_page, error_out=False)
transactions = pagination.items
return jsonify({
'transactions': [t.to_dict() for t in transactions],
'total': pagination.total,
'pages': pagination.pages,
'current_page': page
})
code复制src/
├── api/ # API请求封装
│ ├── auth.js
│ ├── transactions.js
│ └── index.js
├── assets/ # 静态资源
├── components/ # 公共组件
│ ├── charts/
│ ├── forms/
│ └── ...
├── router/ # 路由配置
├── store/ # Vuex状态管理
│ ├── modules/
│ ├── actions.js
│ └── index.js
├── utils/ # 工具函数
├── views/ # 页面组件
│ ├── Auth/
│ ├── Dashboard/
│ └── ...
└── main.js # 应用入口
javascript复制// router/index.js
router.beforeEach((to, from, next) => {
const isAuthenticated = store.getters['auth/isAuthenticated']
if (to.matched.some(record => record.meta.requiresAuth)) {
if (!isAuthenticated) {
next({ name: 'Login', query: { redirect: to.fullPath } })
} else {
next()
}
} else {
next()
}
})
vue复制<template>
<el-form :model="form" :rules="rules" ref="form">
<el-form-item label="类型" prop="type">
<el-radio-group v-model="form.type">
<el-radio-button label="income">收入</el-radio-button>
<el-radio-button label="expense">支出</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item label="金额" prop="amount">
<el-input-number
v-model="form.amount"
:precision="2"
:min="0.01"
controls-position="right"
/>
</el-form-item>
<el-form-item label="分类" prop="category">
<el-select v-model="form.category" filterable>
<el-option
v-for="cat in categories"
:key="cat.value"
:label="cat.label"
:value="cat.value"
/>
</el-select>
</el-form-item>
<el-button type="primary" @click="submitForm">提交</el-button>
</el-form>
</template>
<script>
export default {
data() {
return {
form: {
type: 'expense',
amount: null,
category: '',
note: ''
},
rules: {
type: [{ required: true }],
amount: [
{ required: true, message: '请输入金额' },
{ type: 'number', min: 0.01, message: '金额必须大于0' }
],
category: [{ required: true, message: '请选择分类' }]
},
categories: [
{ value: 'food', label: '餐饮' },
{ value: 'shopping', label: '购物' },
// 其他分类...
]
}
},
methods: {
submitForm() {
this.$refs.form.validate(valid => {
if (valid) {
this.$store.dispatch('transactions/create', this.form)
.then(() => {
this.$message.success('记录添加成功')
this.resetForm()
})
}
})
},
resetForm() {
this.$refs.form.resetFields()
}
}
}
</script>
code复制用户 → Nginx(80)
├── /api → Gunicorn(Flask)
└── / → Vue静态资源
使用Gunicorn作为WSGI服务器:
bash复制pip install gunicorn
gunicorn -w 4 -b 127.0.0.1:8000 app:app
推荐使用Supervisor管理进程:
ini复制[program:finance-api]
command=/path/to/venv/bin/gunicorn -w 4 -b 127.0.0.1:8000 app:app
directory=/path/to/project
user=www-data
autostart=true
autorestart=true
stderr_logfile=/var/log/finance-api.err.log
stdout_logfile=/var/log/finance-api.out.log
生产环境构建:
bash复制npm run build
生成的dist目录包含所有静态资源,配置Nginx服务:
nginx复制server {
listen 80;
server_name yourdomain.com;
root /path/to/dist;
index index.html;
location / {
try_files $uri $uri/ /index.html;
}
location /api {
proxy_pass http://127.0.0.1:8000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
使用Let's Encrypt免费证书:
bash复制sudo apt install certbot python3-certbot-nginx
sudo certbot --nginx -d yourdomain.com
自动续期配置:
bash复制sudo certbot renew --dry-run
使用pytest编写测试用例:
python复制def test_transaction_creation(client, auth):
auth.login()
response = client.post('/api/transactions', json={
'amount': 100,
'type': 'expense',
'category': 'food'
})
assert response.status_code == 201
assert 'id' in response.json
关键测试点:
使用Jest进行单元测试:
javascript复制import { shallowMount } from '@vue/test-utils'
import TransactionForm from '@/components/TransactionForm.vue'
describe('TransactionForm', () => {
it('validates required fields', () => {
const wrapper = shallowMount(TransactionForm)
wrapper.vm.submitForm()
expect(wrapper.vm.$refs.form.validate).toHaveBeenCalled()
})
})
数据库查询优化:
前端性能优化:
API响应优化:
在实际开发过程中,有几个关键点值得特别注意:
JWT令牌刷新机制:最初实现时忽略了token过期问题,导致用户需要频繁重新登录。后来通过添加refresh token机制解决了这个问题。
数据库连接管理:开发初期没有正确关闭数据库连接,导致连接泄漏。最终使用Flask的teardown_appcontext钩子确保连接释放。
前端表单验证:Element UI的表单验证规则需要特别注意异步验证的处理,比如检查用户名是否已存在需要单独处理。
未来可能的扩展方向:
这个项目从技术选型到最终部署,完整展示了如何用Flask+Vue.js技术栈开发一个实用的校园应用。对于想学习全栈开发的同学,建议先从核心功能入手,逐步迭代完善,不要一开始就追求大而全的实现。