一构建Client验证器
1. 客户端注册 包括 app 微信小程序 都属于客户端
2. 客户端种类 很多, 所以注册的形式非常多:短信, 邮件, qq, 微信
新建app/api/v1/clinet.py, 并注册红图(代码不展示了,参考上一篇博文):
from app.libs.redprint import Redprint
api = Redprint('client')
@api.route('/register')
def create_client():
pass
新建app/libs/enums.py
from enum import Enum
class ClientTypeEnum(Enum):
USER_EMAIL = 100
USER_MOBILE = 101
# 微信小程序
USER_MINA = 200
# 微信公众号
USER_WX = 201
新建app/validators/forms.py
from wtforms import Form, StringField, IntegerField
from wtforms.validators import DataRequired, Length
from app.libs.enums import ClientTypeEnum
class ClientForm(Form):
account = StringField(validators=[DataRequired(), Length(5, 32)])
secret = StringField()
type = IntegerField(validators=[DataRequired()])
def validate_type(self, value):
try:
client = ClientTypeEnum(value.data)
except ValueError as e:
raise e
二.处理不同客户端注册的方案
与客户端传输数据的方式一般有两种:
1.在网页中: 表单数据
2.在移动端: json
对于客户端有多种注册的可能性, 我们使用字典的方式来应对, 代码举例讲解:
app/api/v1/client.py
from app.libs.redprint import Redprint
from flask import request
from app.validators.forms import ClientForm
from app.libs.enums import ClientTypeEnum
api = Redprint('client')
@api.route('/register', methosd=['POST'])
def create_client():
data = request.json # 获取json数据的方式request.json 或 request.args.to_dict()
form = ClientForm(data=data)
if form.validate():
promise = {ClientTypeEnum.USER_EMAIL: __register_user_by_email}
# 注册方法名: 对应解决方案
pass
def __register_user_by_email():
pass
三. 创建User模型
新建app/models/user.py, 并将之前flask鱼书中的base.py放入app/models文件夹下
from sqlalchemy import Integer, String, Column, SmallInteger
from werkzeug.security import generate_password_hash
from .base import Base, db
class User(Base):
id = Column(Integer, primary_key=True)
email = Column(String(24), unique=True, nullable=False)
nickname = Column(String(24), unique=True)
auth = Column(SmallInteger, default=1)
_password = Column('password', String(100))
@property
def password(self):
return self._password
@password.setter
def password(self, raw):
self._password = generate_password_hash(raw)
@staticmethod
def register_by_email(nickname, account, secret): # 注册
with db.auto_commit():
user = User()
user.nickname = nickname
user.email = account
user.password = secret
db.session.add(user)
可以继续编写app/api/v1/client.py中的__register_user_by_email:
api = Redprint('client')
@api.route('/register', methosd=['POST'])
def create_client():
data = request.json
form = ClientForm(data=data)
if form.validate():
promise = {ClientTypeEnum.USER_EMAIL: __register_user_by_email}
pass
def __register_user_by_email(form):
User.register_by_email(,form.account.data, form.secret.data) # 第一个参数应该是nickname, 但ClientForm中都是通用型的校验, 没有nickname
User.register_by_email第一个参数应该是nickname, 但ClientForm中都是通用型的校验, 没有nickname。
解决方法在下一节给出。
四. 完成客户端注册
上一节最后的解决方案:在app/validators/form.py中,增加对应的form, 原来的ClientForm作为baseform
app/validators/form.py中新增UserEmailForm
from wtforms import Form, StringField, IntegerField, ValidationError
from wtforms.validators import DataRequired, Length, Email, Regexp
from app.libs.enums import ClientTypeEnum
from app.models.user import User
class ClientForm(Form):
account = StringField(validators=[DataRequired(), Length(5, 32)])
secret = StringField()
type = IntegerField(validators=[DataRequired()])
def validate_type(self, value):
try:
client = ClientTypeEnum(value.data) # 得到枚举类型
except ValueError as e:
raise e
self.type.data = client # type赋值为枚举类型, 不然在client.py中promise字典无法直接出入type.data作为key
class UserEmailForm(ClientForm): # 不同的客户端可以定制化form
account = StringField(validators=[
Email(message='invalidate email')
])
secret = StringField(validators=[DataRequired(), Regexp(r'^[A-Za-z0-9_*&$#@]{6,22}$')])
nickname = StringField(validators=[DataRequired(), Length(min=2, max=22)])
def validate_account(self, value):
if User.query.filter_by(email=value.data).first(): # 数据库中是否已存在
raise ValidationError()
完善app/api/v1/client.py
from app.validators.forms import ClientForm
from app.libs.enums import ClientTypeEnum
from app.models.user import User
from app.validators.forms import UserEmailForm
api = Redprint('client')
@api.route('/register', methosd=['POST'])
def create_client():
data = request.json
form = ClientForm(data=data)
if form.validate():
promise = {ClientTypeEnum.USER_EMAIL: __register_user_by_email} # 存放不同客户端及其对应方法
promise[form.type.data]() # 调用对应方法
return 'success'
def __register_user_by_email():
form = UserEmailForm(data=request.json)
if form.validate():
User.register_by_email(form.nickname.data,form.account.data, form.secret.data)
所有的数据都要经过form验证, 不要直接从request.json获取
五. 生成用户数据
在配置文件中配置mysql连接和SECRET_KEY
运行项目, 在postman中send
运行后,在mysql数据库中看到数据生成完成:
目前写的代码,虽然可以成功运行, 但依然有很多问题, 需要优化。下一节我们来重构一下代码
六. 自定义异常对象
如果我们在postman中send如下数据:
{"account":"[email protected]", "secret":"sjn199367", "type":99, "nickname":"cannon2"}
"type":99
是我们没有定义的枚举类型,点击send后,发现依然返回了success
。但数据库中没有新增任何数据。- 通过调试查找原因, 发现form.validate验证没有通过, 但是却没有报错。我们可以在create_client中自己抛出异常
- 查看werkzeug.exceptions的源码, 发现异常都是继承自HTTPException, 但没有确切的符合目前情况的异常, 所以我们要自定义异常。
新建app/libs/error_code.py
from werkzeug.exceptions import HTTPException
class ClientTypeError(HTTPException):
code = 400 # 选用合适的状态码
description = 'client is invalid'
常见状态码
400 表示请求参数错误, 401未授权, 403禁止访问, 404找不到页面
500 服务器产生未知错误
200 请求成功, 201 创建或更新成功, 204 删除成功
301 重定向
在create_client视图函数中抛出异常,不再依赖form.validate来抛出异常了:
@api.route('/register', methods=['POST'])
def create_client():
data = request.json
form = ClientForm(data=data)
if form.validate():
promise = {ClientTypeEnum.USER_EMAIL: __register_user_by_email} # 存放不同客户端及其对应方法
promise[form.type.data]() # 调用对应方法
else:
raise ClientTypeError #抛出自定义异常
return 'success'
运行后在postman中点击send, 这回得到如下信息
七. 自定义APIException
- 上一节中, 我们自定义异常后, 返回的是html格式的信息(原因可以去HTTPException源码),但是这样的异常信息不太容易定位到错误原因。
- 我们希望返回这样的json格式的错误信息
{"msg":"xxx", "error_code":1000, "request":url}
我们查看HTTPException的部分源代码:
def get_body(self, environ=None):
"""Get the HTML body."""
return text_type((
u'<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 3.2 Final//EN">\n'
u'<title>%(code)s %(name)s</title>\n'
u'<h1>%(name)s</h1>\n'
u'%(description)s\n'
) % {
'code': self.code,
'name': escape(self.name),
'description': self.get_description(environ)
})
def get_headers(self, environ=None):
"""Get a list of headers."""
return [('Content-Type', 'text/html')]
get_body和get_headers这两个HTTPException的方法是返回html格式信息的原因, 我们需要重写它们。
新建app/libs/error.py
from werkzeug.exceptions import HTTPException
from flask import request
import json
class APIException(HTTPException):
# 默认值
code = 500
msg = 'sorry, we make a mistake '
error_code = 999
def __init__(self, msg=None, code=None, error_code=None):
if code:
self.code = code
if error_code:
self.error_code = error_code
if msg:
self.msg = msg
super(APIException, self).__init__(msg, None)
def get_body(self, environ=None): # 让错误页面返回自定义的json内容
body = dict(
msg=self.msg,
error_code=self.error_code,
request=request.method + ' ' + self.get_url_no_param()
)
text = json.dumps(body)
return text
def get_headers(self, environ=None): # 错误页面返回自定的json格式
return [('Content-Type', 'application/json')]
@staticmethod
def get_url_no_param():
full_path = str(request.full_path) #url: /v1/client/register?
main_path = full_path.split('?') #['/v1/client/register', '']
return main_path[0]
让error_code.py中的ClientTypeError改为继承APIException
from .error import APIException
class ClientTypeError(APIException):
# 改写一下对应的默认值
code = 400
error_code = 1006
msg = 'client is invalid'
我们新建app/code.md来存放自定的error_code意义
999 未知错误
1006 client is invalid
运行之后的新的效果: