🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
[TOC] ## 第4章 自定义异常对象 ### 4-1 关于“用户”的思考 ### 4-2 构建 Client 验证器 1. 我们使用枚举类型来代表不同的客户端 ~~~  from enum import Enum  ​  ​  class ClientTypeEnum(Enum):      USER_EMAIL = 100      USER_MOBILE = 101  ​      # 微信小程序      USER_MINA = 200      # 微信公众号      USER_WX = 201 ~~~ 2. 然后再编写自定义的验证器验证客户端传入过来的**客户端类型**,先将客户端传入过来的数字转换成枚举类型 * 如果转换成功,则表示客户端传入进来的数字是正确的 * 如果转换不成功则会报错,表示客户端传入的数字是错误的 ~~~  class ClientForm(Form):      account = StringField(validators=[DataRequired(), length(min=8, max=32)])      secret = StringField()      type = IntegerField(validators=[DataRequired()])  ​      def validate_type(self, value):          """         这里自定义的验证器方法名必须为 'validate_' + 字段名(类变量)         因为 flask 内部设定的,只有这样写才会触发该自定义的验证器验证         :param value: type 传入的具体字段,是 flask 调用验证器的时候自动传入的         :return: 如果在自定义的验证器内抛出异常则表示验证失败;不抛出异常,则表示验证成功(暂时是这么理解的)         """          from app.libs.enums import ClientTypeEnum          try:              client = ClientTypeEnum(value.data)          except ValueError as e:              raise e          self.type.data = client ~~~ **注意**:(查看 `validate_for_api()` 源码发现的) * 这里自定义的验证器方法名必须为 `'validate_' + 字段名`(类变量) * 因为 flask 内部设定的,只有这样写才会触发该自定义的验证器验证 * `:param value:` type 传入的具体字段,是 flask 调用验证器的时候自动传入的 * `:return:` 如果在自定义的验证器内抛出异常则表示验证失败;不抛出异常,则表示验证成功(暂时是这么理解的) * 另外在 pycharm 中会提示 `validate_type`为静态方法,但是实际上这种自定义的验证器方法是实例方法。绝对不能改为静态方法,否则会报错,因为在 `flask` 内部调用的时候是使用 `ClientForm`、`UserEmailForm`、`UserMobileForm`等的实例来调用自定义的验证器的。 ![](https://ws1.sinaimg.cn/large/006tNc79gy1fzgbbz4luyj328k0n87ae.jpg) > 小技巧: > > 在获取客户端传入的数字时,单纯的使用的 value 是获取不到信息的,需要使用 **value.data** 上述代码的精妙之处在于两点: * 我们可以去判断客户端穿过来的数字是否是我们枚举类型的一种 * 客户端传过来的是一个数字的值,但是在我们整个代码编写过程中我们并不希望直接使用数字,因为我们已经定义了枚举类型,所以我们更加希望在项目中使用枚举,因为枚举的可读性比数字要强。 ### 4-3 处理不同客户端注册的方案 #### 提交数据 客户端向服务器发送数据的两种不同的形式: * 表单:通常用于网页中 * JSON 对象:移动端 #### 接收数据 服务器接收参数的方式有两类: * `request.json` (`request.get_json(salient=True)`) * `request.args.to_dict()` 具体的区别稍后再说,我们先使用 request.json 来写: ~~~  data = request.json # 接收到 data  form = ClientForm(data=data) ~~~ 先使用 request.json 接收到 data,然后实例化一个`form`,这个 `form`就是 `ClientForm`。 下面要考虑的就是如何将 data 参数传入 `ClientForm`中,然后 `ClientForm`才能执行校验。我们在**Flask高级编程**中传递客户端的参数是直接把数据放到 `ClientForm`的必填参数中传递进来的。但是如果数据是 JSON 格式的话,就需要使用 `ClientForm`的关键字参数`data=`传参。 > 这种传参方式需要深入挖掘 wtforms 的源码才会知道。 如果数据验证通过的话,就可以进行注册了。但是由于客户端的种类是不同的,不同客户端的注册代码也是不同的。在其他语言中可以使用`switch case`为不用的客户端编写不同的注册代码,但是 python 中是没有 `switch`的,所以需要一些小技巧。 可以使用字典的方式解决。解决方式: ~~~  promise = {              ClientTypeEnum.USER_EMAIL: __register_by_email,              ClientTypeEnum.USER_MOBILE: __register_by_mobile,              ClientTypeEnum.USER_MINA: __register_by_mina,              ClientTypeEnum.USER_WX: __register_by_wx,         } ~~~ 构造字典:{客户端类型:该类型的注册函数} 调用注册函数的方式: ~~~  promise[form.type.data]() ~~~ ### 4-4 创建 User 模型 ~~~  from sqlalchemy import Column, Integer, String, SmallInteger  from werkzeug.security import generate_password_hash  ​  from app.models.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(128))  ​      @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) ~~~ 上面`User`模型的`register_by_email`方法中实例化了 `user`,我们在 `User`对象内部又创建了这个对象本身,从面向对象的角度来说这是不合理的,但是如果该方法是**静态方法**或者**类方法**的话,那么就可以说得通了。静态方法就是跟类、示例无关的方法。类方法就是类的方法,类方法当然可以生成类的实例对象。 ### 4-5 完成客户端注册 ### 4-6 生成用户数据 ### 4-7 自定义异常对象 ![](https://ws4.sinaimg.cn/large/006tNbRwgy1fyqwy1io2vj31hb0o144h.jpg) 我们在 ginger/app/libs/error\_code.py 中自定义异常,在 client.py 中调用就可以了,抛出异常仅仅只是为了显示出程序运行的错误信息而已,并没有其他操作。此处继承的是 APIException 不是 HTTPException。 ~~~  class ClientTypeError(APIException):      # 400 401 403 404      # 500      # 200 201 204      # 301 302      code = 400      msg = 'client is invalid'      error_code = 1006 ~~~ 结果如下所示: ![](https://ws4.sinaimg.cn/large/006tNbRwgy1fyqx39n0d1j32ji0rate5.jpg) ### 4-8 浅谈异常返回的标准与重要性 ![](https://ws4.sinaimg.cn/large/006tNbRwgy1fyqxc7t207j327p0rx0ye.jpg) **一个 API 写的好不好关键就在于你的错误异常信息的表示和描述是否准确、格式是否规范、是否有一个统一的标准。** ### 4-9 自定义APIException 自定义的 APIException 需要继承 HTTPException,同时需要覆写 get\_body、get\_headers 方法,编写 get\_url\_no\_param 方法获取当前访问的不含查询参数的 url。 ~~~  from flask import request, json  from werkzeug.exceptions import HTTPException  ​  ​  ​  class APIException(HTTPException):      code = 500      msg = 'sorry, we made a mistake (* ̄︶ ̄)!'      error_code = 999  ​      def __init__(self, msg=None, code=None, error_code=None,                   headers=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):          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):          """Get a list of headers."""          return [('Content-Type', 'application/json')]  ​      @staticmethod      def get_url_no_param():          full_path = str(request.full_path)          main_path = full_path.split('?')          return main_path[0] ~~~ 字典转化为文本,采用 `json`序列化的方式:`json.dumps()` > `request.full_path():` > > 获取完整的请求路径。