from app.libs.redprint import Redprint
api = Redprint(‘client’)
@api.route(‘/register’)
def create_client():
pass
创建视图函数create_client用于我们针对不同客户端的逻辑处理。这里的redprint是我们在之前的文章《Flask中自定义红图拆分视图函数的方法》所自定义的用于拆分试图函数的对象,在这里我不再赘述。
接下来我们考虑如何对不同的客户端实现注册,是对于每种客户端都各自实现一个注册、登陆等的方法吗?显然这种做法在后面会显得不太简洁且比较凌乱。我们考虑使用枚举定义不同的客户端类型:
from enum import Enum
class ClientTypeEnum(Enum):
USER_EMAIL = 100
USER_MOBILE = 101
USER_MINA = 200 # 微信小程序
USER_WX = 201
回到我们的create_client方法。我们首先考虑,我们需要哪些参数来定义一种客户端类型,如何校验这些参数,以及如何在函数中接收这些参数。根据我们的思路,现在我们用WTForms做参数校验, 在forms.py中定义Form:
from wtforms import StringField, Form, IntegerField
from wtforms.validators import DataRequired, Length
from app.libs.enums import ClientTypeEnum
from app.models.user import User
class ClientForm(Form):
account = StringField(validators=[DataRequired(), length(min=5, max=32)])
secret = StringField()
type = IntegerField(validators=[DataRequired()]) # 客户端类型
def validator_type(self, value):
try:
client = ClientTypeEnum(value.data) # 数字向枚举类型的转换
except ValueErorr as e:
raise e
account和secret对应一般情况下的用户名和密码,type定义客户端类型,这里的type不可以直接接收枚举类型,所以我们选择数字类型,同时我们也将不允许用户随机传入一段数字,而是传入特定枚举类型的数字段,这里type属于我们自定义类型,所以我们需要自定义验证器。我们通过构建validate_type方法来判断用户传入的数字是否属于枚举类型下的数字段,并将用户传过来的数字转换成枚举类型,如果这里没有报错,则成功转换为枚举类型。
ClientForm创建成功后,接着在我们的create_client方法里实例化它:
from app.libs.enums import ClientTypeEnum
from app.libs.redprint import Redprint
from app.models.user import User
from app.validators.forms import ClientForm
api = Redprint('client')
@api.route('', method=['POST'])
def create_client():
data = request.json # 以json的方式接收参数
form = ClientForm(data=data)
if form.validate():
promise = {
ClientTypeEnum.USER_EMAIL:__register_user_by_email,...
}
# 以键调用函数
promise[form.type.data]()
return 'success'
通常客户端向服务端发送数据的两种方式:表单提交和JSON对象提交。表单通常是网页提交数据的方式,而JSON通常是移动端提交数据的方式。
一般我们在实例化表单时的传参的方式是通过把数据放到ClientForm的必填参数中。但如果是json的传参方法,我们需要通过将数据赋值给关键字参数data来传递用户提供的参数。现在我们有了实例化的form后,便可以对校验通过的进行用户注册了,还有一个点需要我们考虑的是,客户端种类繁多,为不同客户端所编写的注册代码也不同,我们如何实现在这种情况下对不同客户端的注册方法的编写呢?在别的语言中可以考虑switch case,我们这里建立一个叫promise的字典,字典下面的每一个键是我们的枚举类型,键所对应的值是该客户端类型的用户注册方法。对于用字典实现其他语言中switch..case的方法,你可以参考我之前的文章:《使用Python字典映射的方式实现其他语言中的switch...case语句》
这里我们对于用电子邮件注册的用户,我们使用__register_user_by_email方法实现它的注册。既然是对用户注册,我们得先实例化我们的Flask-SQLalchemy插件,这里我就不演示了。然后是用户模型的创建,这是我的创建:
# models/user.py
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)
其中auth是我们即将进行对用户的权限标识,默认值为1代表普通用户,2为管理员权限。该模型继承我们自定义的基类模型Base,Base代码如下:
class Base(db.Model):
__abstract__ = True # 使ORM不要创建该模型
create_time = Column(Integer)
status = Column(SmallInteger, default=1) # 对于软删除的支持
def __init__(self):
self.create_time = int(datetime.now().timestamp())
@property
def create_datetime(self):
if self.create_time:
return datetime.fromtimestamp(self.create_time)
else:
return None
def set_attrs(self, attrs_dict):
for key, value in attrs_dict.items():
if hasattr(self, key) and key != 'id':
setattr(self, key, value)
def delete(self): # 软删除
self.status = 0
有了user模型之后,便可以在视图函数中实例化它。我们定义__register_user_by_email方法完成注册操作:
def __register_user_by_email(form): # 从验证器里获取参数
User.register_by_email(form.account.data, form.secret.data)
但这里还有一个参数nickname没办法直接拿到,因为我们定义的ClientForm里没有nickname参数,直接在form里添加吗?针对ClientForm这个对于所有客户端验证表单是不适合添加某个特定客户端种类的属性的。所以我们考虑定义针对email注册用户的表单,这个表单跟ClientForm是总分关系,定义如下:
from wtforms import StringField, Form, IntegerField
from wtforms.validators import DataRequired, Length, Email, ValidationError, Regexp
from app.libs.enums import ClientTypeEnum
from app.models.user import User
class ClientForm(Form):
...
class UserEmailForm(ClientForm):
account = StringField(validators=[
Email(message='invaliate email')
])
secret = StringField(validators=[
DataRequired(),
Regexp(r'^[A-Za-z0-9_*)
])
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()
我们通过编写UserEmailForm,让其继承ClientForm来定义该客户端类型的个性化参数。在通用表单ClientForm里不要求注册形式为email,但在UserEmailForm里,必须为email。对于secret,通用里不强制要求有secret,而在UserEmailForm里需要又secret,并且加上了个性化注册参数nickname,并通过validate_account验证是否已经注册过。ClientForm存在的好处是减少我们的代码量。
现在我们已经拿到了nickname。回到client.py的__register_user_by_email:
def __register_user_by_email(form): # 从验证器里获取参数
form = UserEmailForm(data=request.json)
if form.validate():
User.register_by_email(form.nickname.data,
form.account.data,
form.secret.data)
nickname参数不一定需要从form中拿,我们在定义create_client方法时,data接收的request.json中已经包含了所有的参数,我们可以通过data中去拿,但我们不能直接从data中取拿,而是需要从form里去间接地拿,因为如果直接获取的参数是没有通过我们的校验的。现在create_client()的函数我们就完成了。
还有一点,promise的键是枚举类型,但我们form.type.data得到的是数字类型,我们考虑之前在我们ClientForm中定义了type属性并且在validate_type中已经把数字转换为了枚举类型,我们还需要在转换成了枚举类型之后将值赋值给type。这时,我们得到的才是我们想要的:
class ClientForm(Form):
...
def validate_type(self, value):
...
except ValueError as e:
raise e
self.type.data = client
总结一下:
create_client函数和__register_user_by_email函数像是我们语文中的总分关系。我们定义这种关系来自于客户端种类的多样化,这些众多总类中又有一些共有的参数,那么这些共有参数我们就集中在ClientForm中处理,而各个特色功能的处理集中在分上。如果我们还有其他的注册类型,我们只需要在Promise中增加键值对,且把其他类型的相关处理逻辑写一个新的类似于__register_user_by_email函数就可以了。