最佳实践
json序列化问题
项目中使用sqlalchemy,且路由函数的返回结果是jsonify()
封装的,则如下代码会报错:TypeError: Object of type User is not JSON serializable
@bp_user.route('/login', methods=['POST'])
def login():
""" 登录 """
username = request.json['username']
password = request.json['password']
target_user = User.query.filter(User.username==username, User.password==password).first()
if target_user == None:
return jsonify(status=-1, message='用户名或密码错误', data=None)
return jsonify(status=1, message='登陆成功', data=target_user)
以下是解决步骤(参考):
-
User模型类中,添加
keys()
和__getitem__()
两个方法:class User(db.Model): __tablename__ = 'user' id = db.Column(db.BigInteger, primary_key=True, autoincrement=True) username = db.Column(db.String(256), unique=True ,nullable=False) password = db.Column(db.String(256), nullable=False) create_time = db.Column(db.DateTime, default=datetime.datetime.now) status = db.Column(db.Integer, nullable=False, default=1) def __repr__(self): return 'User{id=%s}' % self.id # 使用静态方法的好处是,可以方便地获取列名,直接调用User.keys()即可 @staticmethod def keys(): return ['id', 'username', 'password', 'create_time', 'status'] def __getitem__(self, item): return getattr(self, item)
-
创建一个encoder.py文件,创建如下类:
import datetime import decimal import uuid from flask.json import JSONEncoder as _JSONEncoder class JSONEncoder(_JSONEncoder): def default(self, o): if hasattr(o, 'keys') and hasattr(o, '__getitem__'): return dict(o) elif isinstance(o, datetime.datetime): return o.strftime('%Y-%m-%d %H:%M:%S') elif isinstance(o, datetime.date): return o.strftime('%Y-%m-%d') elif isinstance(o, decimal.Decimal): return str(o) elif isinstance(o, uuid.UUID): return str(o) elif isinstance(o, bytes): return o.decode('utf-8') else: _JSONEncoder.default(self, o)
-
在app.py(不一定,只要能对app变量操作均可)中,引入上面的
JSONEncoder
并设置给app
变量from utils.encoder import JSONEncoder app.json_encoder = JSONEncoder
-
路由函数(无需变动):
@bp_user.route('/login', methods=['POST']) def login(): """ 登录 """ username = request.json['username'] password = request.json['password'] target_user = User.query.filter(User.username==username, User.password==password).first() if target_user == None: return jsonify(status=-1, message='用户名或密码错误', data=None) return jsonify(status=1, message='登陆成功', data=target_user)
-
测试:
同一个app问题
如果在数据模型db_model.py中实例化了app
变量,这样仅运行db_model.py测试是没问题的。但入口文件app.py中如果也实例化了app
变量,且引用了数据模型User
,则在操作数据的时候可能报错:RuntimeError: The current Flask app is not registered with this 'SQLAlchemy' instance. Did you forget to call 'init_app', or did you create multiple 'SQLAlchemy' instances?
**解决方式:**不要再app.py中重新定义app
变量,而是导入db_model.py的共享即可
# app.py
# app = Flask(__name__, static_folder='../static')
# 修改为下面的
from blueprint.db_model import app
响应流数据
需求
后端:收到请求后,返回csv文件的二进制流
前端:发送请求得到响应后,对收到的二进制流可以做两种操作:
- 保存为本次csv文件
- 不保存到磁盘,而是直接在内存中操作数据。比如直接使用
pd.read_csv(io)
读取这个csv流的内容进行数据分析和处理
后端
@bp_parser.route('/result', methods=['POST'])
def result():
""" 获取解析结果 """
user_id = request.json['user_id']
method = request.json['method']
uuid4 = request.json['uuid4']
template_path = './static/parser/{}_result/{}_{}_structured.csv'.format(method, user_id, uuid4)
# 文件存在性和合法性校验
if not os.path.exists(template_path) or not os.path.isfile(template_path):
return jsonify(status=0, message='未找到此文件', data=None)
return send_file(os.path.abspath(template_path))
前端
保存本地简单;需要注意直接内存读取时,要先将二进制流
response.content
转化为能被pd读取的io流
-
保存为本次csv文件:
import requests url = 'http://127.0.0.1:5000/parser/result' body = { 'user_id': -1, 'method': 'Drain', 'uuid4': '9fe61260-de14-4238-9a0e-f35a284a80af', } response = requests.post(url, json=body) with open('test.csv', 'wb') as fp: fp.write(response.content)
-
直接在内存中操作:
import io import requests import pandas as pd url = 'http://127.0.0.1:5000/parser/result' body = { 'user_id': -1, 'method': 'Drain', 'uuid4': '9fe61260-de14-4238-9a0e-f35a284a80af', } response = requests.post(url, json=body) df = pd.read_csv(io.BytesIO(response.content)) print(df)
解决跨域
-
安装flask-cors
pip install flask-cors
-
包装app对象
from flask import Flask from flask_cors import CORS app = Flask(__name__) CORS(app)
验证码生成及校验
安装captcha库
pip install captcha -i https://mirrors.aliyun.com/pypi/simple
准备验证码字体
从C:\Windows\Fonts\
文件夹下选择一种字体,将其复制到/assets/
文件夹下,例如ALGER.TTF
后端
import os
import random
from captcha.image import ImageCaptcha
from flask import Flask, Response, session, jsonify, request
app = Flask(__name__)
# flask中要使用session会话技术, 必须要配置该参数
app.config['SECRET_KEY'] = os.urandom(24)
# 验证码配置项
VCODE_LENGTH = 4
VCODE_CHARS = 'ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjkmnpqrstuvwxyz23456789'
@app.route('/system/vcode', methods=['GET'])
def generate_vcode():
""" 生成验证码 """
vcode_char_list = random.choices(VCODE_CHARS, k=VCODE_LENGTH)
vcode_text = ''.join(vcode_char_list)
mime_type = 'image/png'
image = ImageCaptcha(fonts=['./assets/ALGER.TTF'])
data = image.generate(vcode_text) # 生成图片验证码
response = Response(data.getvalue(), mimetype=mime_type)
response.headers['Content-Type'] = mime_type
# 统一变成小写存入session
session['vcode'] = vcode_text.lower()
return response
@app.route('/user/login', methods=['POST'])
def login():
""" 登录及验证 """
vcode = request.json.get('vcode')
username = request.json.get('username')
password = request.json.get('password')
# 参数缺失校验
if vcode is None or username is None or password is None:
return jsonify(status=0, message='请求参数缺失', data=None)
# 校验验证码, 统一对照小写
vcode_session = session.get('vcode')
if vcode.lower() != vcode_session:
return jsonify(status=0, message='验证码错误', data=None)
# 校验用户名密码
if username == 'admin' and password == 'admin':
data = {'username': username}
return jsonify(status=1, message='登陆成功', data=data)
return jsonify(status=0, message='用户名或密码错误', data=None)
if __name__ == '__main__':
app.run(debug=False, port=5000)
前端
编写登录api,创建/src/api/user.js
,并编写如下内容:
import request from "../http/request";
export const login = (data) => {
return request({
url: '/user/login',
method: 'post',
data: data
});
};
构建html表单界面:
<el-form ref="loginForm" :model="loginForm">
<div class="login-form-title">
<h1 class="title">满文识别系统</h1>
</div>
<el-form-item prop="account">
<el-input v-model="loginForm.username" type="text" auto-complete="off" placeholder="账号" maxlength="20"
/>
</el-form-item>
<el-form-item prop="password">
<el-input v-model="loginForm.password" type="password" placeholder="密码"
maxlength="20"
@keyup.enter.native="handleLogin"/>
</el-form-item>
<el-form-item prop="vcode">
<el-input v-model="loginForm.vcode" type="text" placeholder="验证码"
maxlength="20"
@keyup.enter.native="handleLogin"/>
</el-form-item>
<el-image id="vcode" :src="vcodeSrc" :fit="'fill'" title="看不清?点击换一张"
@click="handleVcode()"></el-image>
<el-form-item style="width:100%;">
<el-button :loading="loading" class="login-btn" size="medium" type="primary" style="width:100%;"
@click.native.prevent="handleLogin">
<span v-if="!loading">登 录</span>
<span v-else>登 录 中...</span>
</el-button>
</el-form-item>
</el-form>
编写js刷新验证码及登录逻辑:
<script>
import {login} from "@/api/user";
export default {
name: 'Login',
data() {
return {
loginForm: {
username: 'admin',
password: 'admin',
vcode: ''
},
loading: false,
vcodeSrc: '/api/system/vcode',
}
},
methods: {
/** 点击登录按钮 */
handleLogin() {
// 参数完整性校验
if (Object.values(this.loginForm).some((param) => param ==='')) {
return this.$message.error('请填写完整表单');
}
// 发送请求
this.loading = true;
login(this.loginForm).then((res) => {
this.loading = false;
if (res.status === 1) {
// 登录成功
this.$message.success('登陆成功, 欢迎访问');
// 1. 存储 user
localStorage.setItem('user', JSON.stringify(res.data));
// 2. 跳转到后台主页
this.$router.replace('/upload');
} else {
// 登录失败
localStorage.removeItem('user');
// 清空vcode
this.loginForm.vcode = '';
this.$message.error(res.message);
}
}).catch((err) => {
this.loading = false;
this.$message.error('系 统 错 误: ' + err.toString());
});
},
/** 点击验证码图片刷新 */
handleVcode() {
this.vcodeSrc = "/api/system/vcode?time=" + new Date().getTime();
}
}
}
</script>
测试