ThinkChat🤖让你学习和工作更高效,注册即送10W Token,即刻开启你的AI之旅 广告
[TOC] ## 第9章 实现部分鱼书小程序功能 理论必须结合实践,我们提供一个简单的鱼书小程序,编写他的业务接口,并用小程序来进行API的检验 ### 9-1 小程序演示API调用效果 ### 9-2 模糊搜索书籍 1. 新建 `search`视图函数,如何来设计 `search`视图函数呢?既然是搜索图书,必定要吧搜索的参数传到视图函数中。我们以前这些参数的传递要么在 `POST` 中要么在 `URL` 路径中。但是如果说我希望把关键字参数加在问号后面以查询参数的传递方式传递到视图函数中,那么我们该如何接收`q`这个查询参数? ![](https://ws4.sinaimg.cn/large/006tNc79gy1fyza0icwf0j31r106p3zo.jpg) 2. 老规矩:不管参数怎么传递,我们肯定是要验证这个参数。编写个 `Form`来验证它,在 ginger/app/validators/form.py 中: ![](https://ws4.sinaimg.cn/large/006tNc79gy1fyza6r3epij31vg03maas.jpg) 3. 下面我们来看看如何将`q`传入到 `Form`里面。`q`参数该如何得到呢?下面方式可以得到。 ~~~  request.args.to_dict() ~~~ 拿到 `q`之后,怎么传到 `Form`里面去呢?我们可以想一想之前我们把参数放在 `POST`和 `HTTP body`里面的时候是不需要**显式的**传入到 `Form`里面去的。我们是在 ginger/app/validators/base.py 里面通过下面方式拿到 `HTTP body`里面的参数的。 ![](https://ws2.sinaimg.cn/large/006tNc79gy1fyzava76u8j31v806smyb.jpg) 同样的,我们也可以在 `BaseForm`里面完成查询参数的获取,使用`args = request.args.to_dict()`获取参数,再通过`**args`将参数传递给`Form`。(**这中参数传递方法其实看源码就知道了,源码里面写的非常清楚**) ![](https://ws2.sinaimg.cn/large/006tNc79gy1fyzayv2z8qj31r908m75v.jpg) 我们测试会发现能够正确的返回`q`。但是如果不传入 `q`的话则会报错。 ![](https://ws4.sinaimg.cn/large/006tNc79gy1fz08z3yf66j31p20u0dmd.jpg) > 小细节: > > 我们刚刚是没有在 `Headers`中增加 `Content-Type`,但是如果我们在`Headers`增加了`Content-Type`的话,并且将`Content-Type`指定成`application/json`的话,然后再测试就会报一个内置的错误。这个错误的意思大概就是说 flask 尝试去**反序列化**你传递的这个 `json`,但是没有找到你传递的这个 `json`对象,为什么呢?因为我们的 `HTTP body`里面没有任何的 `json`对象,所以说 flask找不到,它就会报这个错误。 > > 为什么不加`Content-Type`就不会报这个错误呢?原因很简单,因为`Content-Type`指的是 `body`里面的参数,你既然制定了`Content-Type`而 `body`里面有没有传递任何值,flask 当然会报错。 > > **解决方法1**:如果你的`body`里面没有`json`对象的话,就不要指定`Content-Type`。 **解决方法2**:在 ginger/app/validators/base.py 中,使用`data = request.get_json(silent=True)`获取`body`的数据即可,括号内的`silent=True`表示静默处理,也就是 `body`为空的时候不报错。 ![](https://ws1.sinaimg.cn/large/006tNc79gy1fz099jshmfj31vk0io0wd.jpg) > > 小知识: > > 如果一个请求是 **POST** 的话,既可以使用 `HTTP body` 传参,也可以在 **url 的查询参数**里传参,甚至你可以在两处同时传参。 4. 新建 ginger/app/module/book.py 文件,内容如下:(`keys`方法为`Book`模型默认的输出字段) ![](https://ws4.sinaimg.cn/large/006tNc79gy1fz09q2zyylj31sm0ritfh.jpg) 5. 现在我们要使用`Book`模型进行数据库的查询和检索,需要注意的是这里要做的查询是**模糊查询**,不能直接等于检索字段查询,所以必须要使用到 `sqlalchemy`的 `like`关键字 ![](https://ws4.sinaimg.cn/large/006tNc79gy1fz0abbh8b5j31sc0c7tbd.jpg) * `q`前后加`%`是因为在 `sql`语句里执行模糊搜索的话,就需要在搜索关键字前后加上`%`; * `or_`:因为默认情况下 `filter`里面的两个条件是 `and`关系,但是现在执行的是模糊检索是`或`关系,所以使用 `sqlalchemy`的`or_`方法; * `like`:`sqlalchemy`模糊检索的方法。 最后完美实现模糊搜索! ### 9-3 再谈严格型REST的缺陷 在 ginger/app/api/v1/book.py 内编写书籍详情页的视图函数 `detail`: ![](https://ws4.sinaimg.cn/large/006tNc79gy1fz0aoasej7j31uj06zabq.jpg) 经过 postman 测试的返回数据,很容易发现上节课`search()`视图函数和`detail()`视图函数返回的数据结中每一本书的字段是相同的,但是仔细回顾本章第一节中演示的小程序示例,对于 `search`来说它需要我们返回那么多字段吗?显然不需要,有一个很明显的地方就是`summary`。`summary`字段是有大量文本的,但是 `summary`在 `search`搜索的结果页面里是完全不需要去展示 `summary`信息的,只有在书籍详情页里面才需要展示书籍详情页的信息。 换句话来说 `search`返回的 `summary`字段是完全没有用的,它占用了我们服务器的带宽,因为这么多大量的文本其实是很浪费服务器带宽的。那么我们现在的理解是在 `search`里面返回 `summary`是不太好的,但是这里就要回到严格型的 RESTFul 的定义上面来。之前就讲过严格型的 REST 的定义是不考虑业务需求需不需要某一个字段的,它是针对资源的,资源里有 `summary`那么查询的时候就一定会将 `summary`返回回来。所以说严格型的 REST 并不太适合内部开发。 怎么解决这个问题呢?我们来看这里有没有什么冲突,我们在 `Book`模型里面其实是定义好了需要返回的额字段的(`keys`方法),但是这个字段是个死的字段任何序列化都会返回这个字段。`search`里面需要精简字段,但是 `detail`里面需要返回所有字段的。所以我们需要在 `search`里面将 `summary`字段去除掉之后再返回: * 解决方法1:因为 Python 语言是动态的,所以你可以去遍历 books 列表,然后将每一个 book 的 summary 字段删除掉,然后再返回; * 解决方法2:可以做一个 `ViewModel`精简字段之后再返回; 但是这两种方法都不太好,如果我们可以在 `search`的 `books` 和 `return` 之间灵活的调整已经定义好的模型字段,把 `summary`删除掉不就行了?例如: ~~~  books = [book.hide('summary', '', '') for book in books] ~~~ 这种方法就很好了,既不影响 `detail`的返回,又可以隐藏字段返回,相当漂亮,那么这种方法该如何实现呢?下节来实现。 ### 9-4 实现hide方法 ![](https://ws4.sinaimg.cn/large/006tNc79gy1fz0bkh7ixij31tf0dntbq.jpg) ![](https://ws2.sinaimg.cn/large/006tNc79gy1fz0bl2a39tj31uc0fe0vb.jpg) * `hide`方法必须 `return self`: * 不 `return`的话,`books`的列表推倒式返回的就是 None,因为列表推倒式的结果取决于`book.hide('summary')`的返回值; * 不能`return fields`,因为`fields`是列表,我们需要返回的是模型对象,后面还要将它序列化,如果返回`fields`列表的话,后面序列化的就是列表,得到的数据会出错。 这样看似已经完美了,其实还有问题,问题在于我们在 postman 里面再次请求调用 `search`的时候会报错,为什么报错呢?下节来分析。 ### 9-5 @orm.reconstructor 解决模型对象实例化问题 ![](https://ws1.sinaimg.cn/large/006tNc79gy1fz0bsr4q0mj32tk0qex22.jpg) 首先我们要搞清楚这个 `list`是什么?这个 `list`其实就是我们定义的 `fields`列表,我们要特别注意的是在第一次 `remove summary`之后没有报错可以正确得到结果,但是在第二次`remove summary`的时候提示找不到这个字段。这里我要说的是,有时候我们一看错误就知道问题是什么,但是有时候错误提示并不是这么直接它需要考验大家对 Python 基本概念的理解。在这个问题当中第一次 `remove`的结果影响了第二次,之所以出现这种影响原因就在于 `fields`定义的是一个**类变量**而不是一个**实例变量**。那这其实就考验大家对类变量和实例变量的理解是否深刻,如果理解的不深刻的话很容易犯这样的错误。类变量的特性是所有实例变量都共有的变量,关键点是共有。 我们这里使用的是**类变量**来定义`fields`,如果 book1 中将 `summary`字段剔除了,那么 book2 中显然就没有 `summary`字段了,所以才会报刚才的错误。 我们应该使用**实例变量**来定义 `fields`字段,这样的话我们在 book1 中修改字段就不会影响到 book2 中的字段。 我们通常在`__init__`中定义实例变量,实例变量将保证每一个实例化后的对象都将拥有单独的 `fields`副本。在 ginger/app/modules/book.py 中: ![](https://ws3.sinaimg.cn/large/006tNc79gy1fz0cdaaeu5j31u306zwfs.jpg) 那么这样写了之后有没有问题?看似是没有问题的,实际上还是有问题的。这个问题可能并不是那么好理解。 如果我们定义一个实例变量,构造函数肯定是要执行的,只有执行了构造函数`self.fields`才能定义成功。但是在该构造函数处打断点发现,`search`视图函数式进到断点这里的,换句话说就是这个构造函数是不会执行的,既然不会执行的话这个实例变量的定义就不会成功。 如果我们在 `search`视图函数内部添加`book = Book()`进行手动实例化的话,断点是可以进来的。但是我要说的是,现在我们得到的 book 的实例对象并不是通过这样实例化创建的,我们现在拿到的实例对象是通过 sqlalchemy 创建的,sqlalchemy 创建实例对象的时候并不是通过普通实例化方法创建的,它是通过**元类**动态创建 book 的。 在 Python 绝大多数的 ORM(sqlalchemy 就是一种 ORM)都是通过元类方式来创建模型对象的。通过元类方式来创建,至少在 sqlalchemy 里面是不会执行构造函数的,那怎么解决这个问题呢?那么这个时候我们就需要用到 sqlalchemy 比较高级的机制,因为它用的比较少,也不能说用的比较少,只不过大多数程序员可能欠缺对代码的追求,所以说很难被用到,所以说在网上搜的时候并不一定能直接搜到解决方案。这就是我所说的,要形成自己代码风格的话有一个必备的能力,就是你要去看文档。文档里面是最原始的、最基础的、最灵活的一些机制的介绍。sqlalchemy 的文档里面其实介绍了关于对象构建和初始化的时候的机制,它明确的说了是不会去执行构造函数的,但是它提供了一种机制`@orm.reconstrustor()`,一旦我们在构造函数上面打上了这个装饰器,那么在模型对象实例化的时候 sqlalchemy 就会执行该构造函数。 ![](https://ws2.sinaimg.cn/large/006tNc79gy1fz0cww0x8jj31s808sjso.jpg) ### 9-6 重构hide与append #### 重构 hide 现在我们只接受一个 `field`字段的话,我们只能隐藏一个字段,那么假如说我们需要隐藏一组字段怎么办呢?在 ginger/app/modules/book.py 中: ![](https://ws2.sinaimg.cn/large/006tNc79gy1fz0d12f8v2j31vm0bwwgc.jpg) 很显然 `keys`、`hide`方法并不属于某一个特定模型的,所以说如果每一个模型都需要隐藏字段的功能搞的话,我们可以将`keys`、`hide`提取到基类 `Base`中。 #### 增加字段 在 ginger/app/modules/base.py 中: ![](https://ws1.sinaimg.cn/large/006tNc79gy1fz0d7ge1ioj31rq06i75d.jpg) ### 9-7 赠送礼物接口 新建 ginger/app/api/v1/gift.py 文件,创建并注册 `gift RedPront` ![](https://ws4.sinaimg.cn/large/006tNc79gy1fz0feh1r1zj31qy0o2gp7.jpg) ![](https://ws3.sinaimg.cn/large/006tNc79gy1fz0fd9hjnej31va0tejwp.jpg) 注意点: 1. 创建并注册 `gift` 红图; 2. 定义路由,必须包含 `isbn`; 3. 添加登录保护; 4. 从登录保护里获取当前登录用户的 `uid`; 5. 先要确保要添加心愿的书籍存在; 6. 再根据 `uid`、`isbn`从 `gift`表查询礼物,判断当前 `gift` 是否已经存在,如果存在则返回 `gift` 已经存在心愿清单; 7. 如果不存在,则实例化 `gift`,将新 `gift`添加到数据库。 ### 9-8 实现获取令牌信息接口 我们之前写了 `get_token`获取令牌的接口,那么问题来了:如果我想验证一个令牌是否过期的话,该怎么办呢?目前来说我们没有提供验证令牌的接口,下面我们就在 ginger/app/api/v1/token.py 中添加一个**获取令牌信息的接口**: ![](https://ws3.sinaimg.cn/large/006tNc79gy1fz0hg5lx7cj31or0u0ai4.jpg) 在 ginger/app/validators/forms.py 中: ![](https://ws4.sinaimg.cn/large/006tNc79gy1fzamh9bazrj31sa03e0te.jpg) 注意点: 1. 设计路由,该接口不需要登录保护 2. `token`从`HTTP body`里获取,所以需要表单验证 3. 获取 `token` 4. 反序列化 `token`,进行**有效期验证报错**和**token 合法性验证报错** 5. 将时间`token`时间戳格式的生成时间`create_at`、过期时间`expire_in`转为可视化的时间,使用 `time`模块: ~~~  可视化时间 = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime(时间戳)) ~~~ * `data[1]['iat']`是序列化时自动添加进去的生成时间 * `data[1]['exp']`是序列化时自动添加进去的有效时间 6. 将返回信息打包到字典 7. 返回序列化后的字典