第十二章 构建API

12 构建API

在上一章中,你构建了一个学生注册和课程报名系统。你创建了显示课程内容的视图,并学习了如何使用Django的缓存框架。在本章中,你会学习以下知识点:

  • 构建一个RESTful API
  • 为API视图处理认证和权限
  • 创建API视图集和路由

12.1 构建RESTful API

你可能想要创建一个接口,让其它服务可以与你的web应用交互。通过构建一个API,你可以允许第三方以编程方式使用信息和操作你的应用。

你可以通过很多方式构建API,但最好是遵循REST原则。REST架构是表述性状态传递(Representational State Transfer)的缩写。RESTful API是基于资源的。你的模型代表资源,HTTP方法(比如GET,POST,PUT或DELETE)用于检索,创建,更新或者删除对象。HTTP响应代码也可以在这个上下文中使用。返回的不同HTTP响应代码表示HTTP请求的结果,比如2XX响应代码表示成功,4XX表示错误等等。

RESTful API最常用的交互数据的格式是JSON和XML。我们将为项目构建一个JSON序列化的REST API。我们的API会提供以下功能:

  • 检索主题
  • 检索可用的课程
  • 检索课程内容
  • 报名参加课程

我们可以通过Django创建自定义视图,从头开始构建API。但是有很多第三方模块可以简化创建API,其中最流行的是Django Rest Framework

12.1.1 安装Django Rest Framework

Django Rest Framework可以很容易的为项目构建REST API。你可以在这里查看所有文档。

打开终端,使用以下命令安装框架:

pip install djangorestframework

编辑educa项目的settings.py文件,在INSTALLED_APPS设置中添加rest_framework

INSTALLED_APPS = [
    # ...
    'rest_framework',
]

然后在settings.py文件中添加以下代码:

REST_FRAMEWORK = {
    'DEFAULT_PREMISSION_CLASSES': [
        'rest_framework.permissions.DjangoModelPermissionsOrAnonReadOnly'
    ]
}

你可以使用REST_FRAMEWORK设置为API提供一个特定配置。REST Framework提供了大量设置来配置默认行为。DEFAULT_PREMISSION_CLASSES设置指定读取,创建,更新或者删除对象的默认权限。我们设置DjangoModelPermissionsOrAnonReadOnly是唯一的默认权限类。这个类依赖Django的权限系统,允许用户创建,更新或删除对象,同时为匿名用户提供只读访问。之后你会学习更多关于权限的内容。

你可以访问这里查看完整的REST Framework可用设置列表。

12.1.2 定义序列化器

设置REST Framework之后,我们需要指定如何序列化我们的数据。输出数据必须序列化为指定格式,输入数据会反序列化处理。框架为单个类构建序列化器提供了以下类:

  • Serializer:为普通Python类实例提供序列化
  • ModelSerializer:为模型实例提供序列化
  • HyperlinkedModelSerializer:与ModelSerializer一样,但使用链接而不是主键表示对象关系

让我们构建第一个序列化器。在courses应用目录中创建以下文件结构:

api/
    __init__.py
    serializers.py

我们会在api目录中构建所有API功能,保持良好的文件结构。编辑serializers.py文件,并添加以下代码:

from rest_framework import serializers
from ..models import Subject

class SubjectSerializer(serializers.ModelSerializer):
    class Meta:
        model = Subject
        fields = ('id', 'title', 'slug')

这是Subject模型的序列化器。序列化器的定义类似于Django的FormModelForm类。Meta类允许你指定序列化的模型和序列化中包括的字段。如果没有设置fields属性,则会包括所有模型字段。

让我们试试序列化器。打开终端执行python manage.py shell命令,然后执行以下代码:

>>> from courses.models import Subject
>>> from courses.api.serializers import SubjectSerializer
>>> subject = Subject.objects.latest('id')
>>> serializer = SubjectSerializer(subject)
>>> serializer.data

在这个例子中,我们获得一个Subject对象,创建一个SubjectSerializer实例,然后访问序列化的数据。你会看到以下输出:

{'id': 4, 'slug': 'mathematics', 'title': 'Mathematics'}

正如你所看到的,模型数据转换为Python的原生数据类型。

12.1.3 理解解析器和渲染器

在HTTP响应中返回序列化的数据之前,需要把它渲染为特定格式。同样的,当你获得HTTP请求时,在你操作它之前,需要解析传入的数据并反序列化数据。REST Framework包括渲染器和解析器来处理这些操作。

让我们看看如何解析收到的数据。给定一个JSON字符串输入,你可以使用REST Framework提供的JSONParser类转换为Python对象。在Python终端中执行以下代码:

from io import BytesIO
from rest_framework.parsers import JSONParser
data = b'{"id":4,"title":"Music","slug":"music"}'
JSONParser().parse(BytesIO(data))

你会看到以下输出:

{'id': 4, 'title': 'Music', 'slug': 'music'}

REST Framework还包括Renderer类,允许你格式化API响应。框架通过内容协商决定使用哪个渲染器。它检查请求的Accept头,决定响应期望的内容类型。根据情况,渲染器由URL格式后缀确定。例如,触发JSONRenderer的访问会返回JSON响应。

回到终端执行以下代码,从上一个序列化器例子中渲染serializer对象:

>>> from rest_framework.renderers import JSONRenderer
>>> JSONRenderer().render(serializer.data)

你会看到以下输出:

b'{"id":4,"title":"Mathematics","slug":"mathematics"}'

我们使用JSONRenderer渲染序列化的数据位JSON。默认情况下,REST Framework使用两个不同的渲染器:JSONRendererBrowsableAPIRenderer。后者提供一个web接口,可以很容易的浏览你的API。你可以在REST_FRAMEWORK设置的DEFAULT_RENDERER_CLASSES选项中修改默认的渲染器类。

你可以查看更多关于渲染器解析器的信息。

12.1.4 构建列表和详情视图

REST Framework自带一组构建API的通用视图和mixins。它们提供了检索,创建,更新或删除模型对象的功能。你可以在这里查看REST Framework提供的所有通用的mixins和视图。

让我们创建检索Subject对象的列表和详情视图。在courses/api/目录中创建views.py文件,并添加以下代码:

from rest_framework import generics
from ..models import Subject
from .serializers import SubjectSerializer

class SubjectListView(generics.ListAPIView):
    queryset = Subject.objects.all()
    serializer_class = SubjectSerializer

class SubjectDetailView(generics.RetrieveAPIView):
    queryset = Subject.objects.all()
    serializer_class = SubjectSerializer

在这段代码中,我们使用了REST Framework的通用ListAPIViewRetrieveAPIView。我们在详情视图中包括一个pk URL参数,来检索给定主键的对象。两个视图都包括以下属性:

  • queryset:用于检索对象的基础QuerySet
  • serializer_class:序列化对象的类。

让我们为视图添加URL模式。在courses/api/目录中创建urls.py文件,并添加以下代码:

from django.conf.urls import url
from . import views

urlpatterns = [
    url(r'^subjects/$', views.SubjectListView.as_view(), name='subject_list'),
    url(r'^subjects/(?P<pk>\d+)/$', views.SubjectDetailView.as_view(), name='subject_detail'),
]

编辑educa项目的主urls.py文件,并引入API模式:

urlpatterns = [
    # ...
    url(r'^api/', include('courses.api.urls', namespace='api')),
]

我们为API的URL使用api命名空间。使用python manage.py runserver启动开发服务器。打开终端,并使用curl获取http://127.0.0.1:8000/api/subjects/

bogon:educa lakerszhy$ curl http://127.0.0.1:8000/api/subjects/

你会看到类似以下的响应:

[{"id":4,"title":"Mathematics","slug":"mathematics"},
{"id":3,"title":"Music","slug":"music"},
{"id":2,"title":"Physics","slug":"physics"},
{"id":1,"title":"Programming","slug":"programming"}]

HTTP响应包括JSON格式的Subject对象列表。如果你的操作系统没有安装curl,请在这里下载。除了curl,你还可以使用其它工具发送自定义HTTP请求,比如浏览器扩展Postman,你可以在这里下载Postman

在浏览器中打开http://127.0.0.1:8000/api/subjects/。你会看到REST Framework的可浏览API,如下图所示:

这个HTML界面由BrowsableAPIRenderer渲染器提供。你还可以在URL中包括id来访问一个Subject对象的API详情视图。在浏览器中打开http://127.0.0.1:8000/api/subjects/1/。你会看到单个Subject对象以JSON格式渲染。

12.1.5 创建嵌套的序列化器

我们将为Course模型创建一个序列化器。编辑api/serializers.py文件,并添加以下代码:

from ..models import Course

class CourseSerializer(serializers.ModelSerializer):
    class Meta:
        model = Course
        fields = ('id', 'subject', 'title', 'slug', 
            'overview', 'created', 'owner', 'modules')

让我们看看一个Course对象是如何被序列化的。在终端执行python manage.py shell,然后执行以下代码:

>>> from rest_framework.renderers import JSONRenderer
>>> from courses.models import Course
>>> from courses.api.serializers import CourseSerializer
>>> course = Course.objects.latest('id')
>>> serializer = CourseSerializer(course)
>>> JSONRenderer().render(serializer.data)

你获得的JSON对象包括我们在CourseSerializer中指定的字段。你会看到modules管理器的关联对象被序列化为主键列表,如下所示:

"modules": [17, 18, 19, 20, 21, 22]

我们想包括每个单元的更多信息,所以我们需要序列化Module对象,并且嵌套它们。修改api/serializers.py文件中的上一段代码,如下所示:

from ..models import Course, Module

class ModuleSerializer(serializers.ModelSerializer):
    class Meta:
        model = Module
        fields = ('order', 'title', 'description')

class CourseSerializer(serializers.ModelSerializer):
    modules = ModuleSerializer(many=True, read_only=True)
    
    class Meta:
        model = Course
        fields = ('id', 'subject', 'title', 'slug', 
            'overview', 'created', 'owner', 'modules')

我们定义了ModuleSerializer,为Module模型提供了序列化。然后我们添加modules属性到CourseSerializer来嵌套ModuleSerializer序列化器。我们设置many=True表示正在序列化的是多个对象。read_only参数表示该字段是可读的,并且不应该包括在任何输入中来创建或更新对象。

打开终端,并再创建一个CourseSerializer实例。使用JSONRenderer渲染序列化器的data属性。这次,单元列表被嵌套的ModuleSerializer序列化器序列化,如下所示:

"modules": [
    {
        "order": 0,
        "title": "Django overview",
        "description": "A brief overview about the Web Framework."
    }, 
    {
        "order": 1,
        "title": "Installing Django",
        "description": "How to install Django."
    },
    ...

你可以在这里阅读更多关于序列化器的信息。

12.1.6 构建自定义视图

REST Framework提供了一个APIView类,可以在Django的View类之上构建API功能。APIView类与View类不同,它使用REST Framework的自定义RequestResponse对象,并且处理APIException异常返回相应的HTTP响应。它还包括一个内置的认证和授权系统来管理视图的访问。

我们将为用户创建课程报名的视图。编辑api/views.py文件,并添加以下代码:

from django.shortcuts import get_object_or_404
from rest_framework.views import APIView
from rest_framework.response import Response
from ..models import Course

class CourseEnrollView(APIView):
    def post(self, request, pk, format=None):
        course = get_object_or_404(Course, pk=pk)
        course.students.add(request.user)
        return Response({'enrolled': True})

CourseEnrollView视图处理用户报名参加课程。上面的代码完成以下任务:

  • 我们创建了一个继承自APIView的自定义视图。
  • 我们为POST操作定义了post()方法。这个视图不允许其它HTTP方法。
  • 我们期望URL参数pk包含课程ID。我们用给定的pk参数检索课程,如果没有找到则抛出404异常。
  • 我们添加当前对象到Course对象的多对多关系students中,并返回成功的响应。

编辑api/urls.py文件,并为CourseEnrollView视图添加URL模式:

url(r'^courses/(?P<pk>\d+)/enroll/$', views.CourseEnrollView.as_view(), name='course_enroll'),

理论上,我们现在可以执行一个POST请求,为当前用户报名参加一个课程。但是,我们需要识别用户,并阻止未认证用户访问这个视图。让我们看看API认证和权限是如何工作的。

12.1.7 处理认证

REST Framework提供了识别执行请求用户的认证类。如果认证成功,框架会在request.user中设置认证的User对象。否则设置为Django的AnonymousUser实例。

REST Framework提供以下认证后台:

  • BasicAuthentication:HTTP基础认证。客户端用Base64在Authorization HTTP头中发送用户和密码。你可以在这里进一步学习。
  • TokenAuthentication:基于令牌的认证。一个Token模型用于存储用户令牌。用户在Authorization HTTP头中包括用于认证的令牌。
  • SessionAuthentication:使用Django的会话后台用于认证。当执行从你的网站前端到API的AJAX请求时,这个后台非常有用。

你可以通过继承REST Framework提供的BaseAuthentication类,并覆写authenticate()方法来构建自定义认证后台。

你可以基于单个视图设置认证,或者用DEFAULT_AUTHENTICATION_CLASSES设置为全局认证。

认证只识别执行请求的用户。它不会允许或阻止访问视图。你必须使用权限来显示访问视图。

你可以在这里查看所有关于认证的信息。

让我们添加BasicAuthentication到我们的视图。编辑courses应用的api/views.py文件,并添加authentication_classes属性到CourseEnrollView

from rest_framework.authentication import BasicAuthentication

class CourseEnrollView(APIView):
    authentication_classes = (BasicAuthentication, )
    # ...

用户将通过设置在HTTP请求中的Authorization头的证书识别。

12.1.8 添加权限到视图

REST Framework包括一个权限系统,用于限制视图的访问。REST Framework的一些内置权限是:

  • AllowAny:不限制访问,不管用户是否认证。
  • IsAuthenticated:只允许认证的用户访问。
  • IsAuthenticatedOrReadOnly:认证用户可以完全访问。匿名用户只允许执行读取方法,比如GET,HEAD或OPTIONS。
  • DjangoModelPermissions:捆绑到django.contrib.auth的权限。视图需要一个queryset属性。只有分配了模型权限的认证用户才能获得权限。
  • DjangoObjectPermissions:基于单个对象的Django权限。

如果用户被拒绝访问,他们通常会获得以下某个HTTP错误代码:

  • HTTP 401:未认证
  • HTTP 403:没有权限

你可以在这里阅读更多关于权限的信息。

编辑courses应用的api/views.py文件,并在CourseEnrollView中添加permission_classes属性:

from rest_framework.authentication import BasicAuthentication
from rest_framework.permissions import IsAuthenticated

class CourseEnrollView(APIView):
    authentication_classes = (BasicAuthentication, )
    permission_classes = (IsAuthenticated, )
    # ..

我们引入了IsAuthenticated权限。这会阻止匿名用户访问这个视图。现在我们可以执行POST请求到新的API方法。

确保开发服务器正在运行。打开终端并执行以下命令:

curl -i -X POST http://127.0.0.1:8000/api/courses/1/enroll/

你会获得以下响应:

HTTP/1.0 401 UNAUTHORIZED
...
{"detail": "Authentication credentials were not provided."}

因为我们是未认证用户,所以如期获得401 HTTP代码。让我们用其中一个用户进行基础认证。执行以下命令:

curl -i -X POST -u student:password http://127.0.0.1:8000/api/courses/1/enroll/

用已存在用户凭证替换student:password。你会获得以下响应:

HTTP/1.0 200 OK
...
{"enrolled": true}

你可以访问管理站点,检查用户是否报名参加课程。

12.1.9 创建视图集和路由

ViewSets允许你定义你的API交互,并让REST Framework用Router对象动态构建URL。通过视图集,你可以避免多个视图的重复逻辑。视图集包括典型的创建,检索,更新,删除操作,分别是list()create()retrieve()update()partial_update()destroy()

让我们为Course模型创建一个视图集。编辑api/views.py文件,并添加以下代码:

from rest_framework import viewsets
from .serializers import CourseSerializer

class CourseViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = Course.objects.all()
    serializer_class = CourseSerializer

我们从ReadOnlyModelViewSet继承,它提供了只读操作list()retrieve(),用于列出对象或检索单个对象。编辑api/urls.py文件,并为我们的视图集创建一个路由:

from django.conf.urls import url, include
from . import views
from rest_framework import routers

router = routers.DefaultRouter()
router.register('courses', views.CourseViewSet)

urlpatterns = [
    # ...
    url(r'^', include(router.urls)),
]

我们创建了一个DefaultRouter对象,并用courses前缀注册我们的视图集。路由负责为我们的视图集自动生成URL。

在浏览器中打开http://127.0.0.1:8000/api/。你会看到路由在它的基础URL中列出所有视图集,如下图所示:

你现在可以访问http://127.0.0.1:8000/api/courses/检索课程列表。

你可以在这里进一步学习视图集。你还可以在这里查看更多关于路由的信息。

12.1.10 添加额外操作到视图集

你可以添加额外操作到视图集中。让我们把之前的CourseEnrollView视图为一个自定义视图集操作。编辑api/views.py文件,并修改CourseViewSet类:

from rest_framework.decorators import detail_route

class CourseViewSet(viewsets.ReadOnlyModelViewSet):
    queryset = Course.objects.all()
    serializer_class = CourseSerializer

    @detail_route(
        methods=['post'],
        authentication_classes=[BasicAuthentication],
        permission_classes=[IsAuthenticated]
    )
    def enroll(self, request, *args, **kwargs):
        course = self.get_object()
        course.students.add(request.user)
        return Response({'enrolled': True})

我们添加了一个自定义的enroll()方法,它代表这个视图集的一个额外操作。上面的代码执行以下任务:

  • 我们使用框架的detail_route装饰器,指定这是在单个对象上执行的操作。
  • 装饰器允许我们为操作添加自定义属性。我们指定这个视图只允许POST方法,并设置了认证和权限类。
  • 我们使用self.get_object()检索Courses对象。
  • 我们把当前用户添加到students多对多关系中,并返回一个自定义的成功响应。

编辑api/urls.py文件,移除以下URL,因为我们不再需要它:

url(r'^courses/(?P<pk>\d+)/enroll/$', views.CourseEnrollView.as_view(), name='course_enroll'),

然后编辑api/views.py文件,移除CourseEnrollView类。

现在,报名参加课程的URL由路由自动生成。因为它使用操作名enroll,所以URL保持不变。

12.1.11 创建自定义权限

我们希望学生可以访问它们报名的课程内容。只有报名的学生才可以访问课程内容。最好的实现方式是使用一个自定义权限类。Django提供的BasePermission类允许你定义以下方法:

  • has_permission():视图级别的权限检查
  • has_object_permission():实例级别的权限检查

如果获得访问权限,这些方法返回True,否则返回False。在courses/api/目录中创建permissions.py文件,并添加以下代码:

from rest_framework.permissions import BasePermission

class IsEnrolled(BasePermission):
    def has_object_permission(self, request, view, obj):
        return obj.students.filter(id=request.user.id).exists()

我们从BasePermission类继承,并覆写has_object_permission()。我们检查执行请求的用户是否存在Course对象的students关系中。我们下一步会使用IsEnrolled权限。

12.1.12 序列化课程内容

我们需要序列化课程内容。Content模型包括一个通用外键,允许我们访问关联对象的不同内容模型。但是,我们在上一章为所有内容模型添加了通用的render()方法。我们可以使用这个方法为API提供渲染后的内容。

编辑courses应用的api/serializers.py文件,并添加以下代码:

from ..models import Content

class ItemRelatedField(serializers.RelatedField):
    def to_representation(self, value):
        return value.render()

class ContentSerializer(serializers.ModelSerializer):
    item = ItemRelatedField(read_only=True)

    class Meta:
        model = Content
        fields = ('order', 'item')

在这段代码中,通过继承REST Framework提供的RelatedField序列化器字段和覆写to_representation()方法,我们定义了一个自定义字段。我们为Content模型定义了ContentSerializer序列化器,并用自定义字段作为item通用外键。

我们需要一个包括内容的Module模型的替换序列化器,以及一个扩展的Course序列化器。编辑api/serializers.py文件,并添加以下代码:

class ModuleWithContentsSerializer(serializers.ModelSerializer):
    contents = ContentSerializer(many=True)

    class Meta:
        model = Module
        fields = ('order', 'title', 'description', 'contents')

class CourseWithContentsSerializer(serializers.ModelSerializer):
    modules = ModuleWithContentsSerializer(many=True)

    class Meta:
        model = Course
        fields = ('id', 'subject', 'title', 'slug', 'overview', 
            'created', 'owner', 'modules')

让我们创建一个模仿retrieve()操作,但是包括课程内容的视图。编辑api/views.py文件,并在CourseViewSet类中添加以下方法:

from .permissions import IsEnrolled
from .serializers import CourseWithContentsSerializer

class CourseViewSet(viewsets.ReadOnlyModelViewSet):
    # ...
    @detail_route(
        methods=['get'],
        serializer_class=CourseWithContentsSerializer,
        authentication_classes=[BasicAuthentication],
        permission_classes=[IsAuthenticated, IsEnrolled]
    )
    def contents(self, request, *args, **kwargs):
        return self.retrieve(request, *args, **kwargs)

这个方法执行以下任务:

  • 我们使用detail_route装饰器指定该操作在单个对象上执行。
  • 我们指定该操作只允许GET方法。
  • 我们使用新的CourseWithContentsSerializer序列化器类,它包括渲染的课程内容。
  • 我们使用IsAuthenticated和自定义的IsEnrolled权限。这样可以确保只有报名的用户可以访问课程内容。
  • 我们使用存在的retrieve()操作返回课程对象。

在浏览器中打开http://127.0.0.1:8000/api/courses/1/contents/。如果你用正确证书访问视图,你会看到课程的每个单元,包括渲染后的课程内容的HTML,如下所示:

{
   "order": 0,
   "title": "Installing Django",
   "description": "",
   "contents": [
        {
        "order": 0,
        "item": "<p>Take a look at the following video for installing Django:</p>\n"
        }, 
        {
        "order": 1,
        "item": "\n<iframe width=\"480\" height=\"360\" src=\"http://www.youtube.com/embed/bgV39DlmZ2U?wmode=opaque\" frameborder=\"0\" allowfullscreen></iframe>\n\n"
        } 
    ]
    }

你已经构建了一个简单的API,允许其它服务通过编程方式访问course应用。REST Framework还允许你用ModelViewSet视图集管理创建和编辑对象。我们已经学习了Django Rest Framework的主要部分,但你仍可以在这里进一步学习它的特性。

12.2 总结

在这一章中,你创建了一个RESTful API,可以让其它服务与你的web应用交互。

额外的第十三章可以在这里下载。它教你如何使用uWSGINGINX构建一个生产环境。你还会学习如何实现一个自定义的中间件和创建自定义的管理命令。

你已经到达了本书的结尾。恭喜你!你已经学会了用Django构建一个成功的web应用所需要的技巧。本书指导你完成开发实际项目,以及将Django与其它技术结合。现在你已经准备好创建自己的Django项目,不管是一个简单的原型还是一个大型的web应用。

祝你下一次Django冒险活动好运!

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 211,884评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,347评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,435评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,509评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,611评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,837评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,987评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,730评论 0 267
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,194评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,525评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,664评论 1 340
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,334评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,944评论 3 313
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,764评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,997评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,389评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,554评论 2 349

推荐阅读更多精彩内容