第十章 构建一个在线学习平台(上)

10 构建一个在线学习平台

在上一章中,你为在线商店项目添加了国际化。你还构建了一个优惠券系统和一个商品推荐引擎。在本章中,你会创建一个新的项目。你会构建一个在线学习平台,这个平台会创建一个自定义的内容管理系统。

在本章中,你会学习如何:

  • 为模型创建fixtures
  • 使用模型继承
  • 创建自定义O型字典
  • 使用基于类的视图和mixins
  • 构建表单集
  • 管理组和权限
  • 创建一个内容管理系统

10.1 创建一个在线学习平台

我们最后一个实战项目是一个在线学习平台。在本章中,我们会构建一个灵活的内容管理系统(CMS),允许教师创建课程和管理课程内容。

首先,我们用以下命令为新项目创建一个虚拟环境,并激活它:

mkdir env
virtualenv env/educa
source env/educa/bin/activate

用以下命令在虚拟环境中安装Django:

pip install Django

我们将在项目中管理图片上传,所以我们还需要用以下命令安装Pillow:

pip install Pillow

使用以下命令创建一个新项目:

django-admin startproject educa

进入新的educa目录,并用以下命令创建一个新应用:

cd educa
django-admin startapp courses

编辑educa项目的settings.py文件,把courses添加到INSTALLED_APPS设置中:

INSTALLED_APPS = [
    'courses',
    'django.contrib.admin',
    'django.contrib.auth',
    'django.contrib.contenttypes',
    'django.contrib.sessions',
    'django.contrib.messages',
    'django.contrib.staticfiles',
]

现在courses应用已经在项目激活了。让我们为课程和课程内容定义模型。

10.2 构建课程模型

我们的在线学习平台会提供多种主题的课程。每个课程会划分为可配置的单元数量,而每个单元会包括可配置的内容数量。会有各种类型的内容:文本,文件,图片或者视频。下面这个例子展示了我们的课程目录的数据结构:

Subject 1
    Course 1
        Module 1
            Content 1 (image)
            Content 3 (text)
        Module 2
            Content 4 (text)
            Content 5 (file)
            Content 6 (video)
            ...

让我们构建课程模型。编辑courses应用的models.py文件,并添加以下代码:

from django.db import models
from django.contrib.auth.models import User

class Subject(models.Model):
    title = models.CharField(max_length=200)
    slug = models.SlugField(max_length=200, unique=True)

    class Meta:
        ordering = ('title', )

    def __str__(self):
        return self.title

class Course(models.Model):
    owner = models.ForeignKey(User, related_name='courses_created')
    subject = models.ForeignKey(Subject, related_name='courses')
    title = models.CharField(max_length=200)
    slug = models.SlugField(max_length=200, unique=True)
    overview = models.TextField()
    created = models.DateTimeField(auto_now_add=True)

    class Meta:
        ordering = ('-created',)

    def __str__(self):
        return self.title

class Module(models.Model):
    course = models.ForeignKey(Course, related_name='modules')
    title = models.CharField(max_length=200)
    description = models.TextField(blank=True)

    def __str__(self):
        return self.title

这些是初始的SubjectCourseModule模型。Course模型有以下字段:

  • owner:创建给课程的教师
  • subject:这个课程所属的主题。一个指向Subject模型的ForeignKey字段。
  • title:课程标题.
  • slug:课程别名,之后在URL中使用。
  • overview:一个TextField列,表示课程概述。
  • created:课程创建的日期和时间。因为设置了auto_now_add=True,所以创建新对象时,Django会自动设置这个字段。

每个课程划分为数个单元。因此,Module模型包含一个指向Course模型的ForeignKey字段。

打开终端执行以下命令,为应用创建初始的数据库迁移:

python manage.py makemigrations

你会看到以下输出:

Migrations for 'courses':
  courses/migrations/0001_initial.py
    - Create model Course
    - Create model Module
    - Create model Subject
    - Add field subject to course

然后执行以下命令,同步迁移到数据库中:

python manage.py migrate

你会看到一个输出,其中包括所有已经生效的数据库迁移,包括Django的数据库迁移。输出会包括这一行:

Applying courses.0001_initial... OK

这个告诉我们,courses应用的模型已经同步到数据库中。

10.2.1 在管理站点注册模型

我们将把课程模型添加到管理站点。编辑courses应用目录中的admin.py文件,并添加以下代码:

from django.contrib import admin
from .models import Subject, Course, Module

@admin.register(Subject)
class SubjectAdmin(admin.ModelAdmin):
    list_display = ['title', 'slug']
    prepopulated_fields = {'slug': ('title', )}

class ModuleInline(admin.StackedInline):
    model = Module

@admin.register(Course)
class CourseAdmin(admin.ModelAdmin):
    list_display = ['title', 'subject', 'created']
    list_filter = ['created', 'subject']
    search_fields = ['title', 'overview']
    prepopulated_fields = {'slug': ('title', )}
    inlines = [ModuleInline]

现在courses应用的模型已经在管理站点注册。我们用@admin.register()装饰器代替admin.site.register()函数。它们的功能是一样的。

10.2.2 为模型提供初始数据

有时你可能希望用硬编码数据预填充数据库。这在项目创建时自动包括初始数据很有用,来替代手工添加数据。Django自带一种简单的方式,可以从数据库中加载和转储(dump)数据到fixtures文件中。

Django支持JSON,XML或者YAML格式的fixtures。我们将创建一个fixture,其中包括一些项目的初始Subject对象。

首先使用以下命令创建一个超级用户:

python manage.py createsuperuser

然后用以下命令启动开发服务器:

python manage.py runserver

现在在浏览器中打开http://127.0.0.1:8000/admin/courses/subject/。使用管理站点创建几个主题。列表显示页面如下图所示:

在终端执行以下命令:

python manage.py dumpdata courses --indent=2

你会看到类似这样的输出:

[
{
  "model": "courses.subject",
  "pk": 1,
  "fields": {
    "title": "Programming",
    "slug": "programming"
  }
},
{
  "model": "courses.subject",
  "pk": 2,
  "fields": {
    "title": "Physics",
    "slug": "physics"
  }
},
{
  "model": "courses.subject",
  "pk": 3,
  "fields": {
    "title": "Music",
    "slug": "music"
  }
},
{
  "model": "courses.subject",
  "pk": 4,
  "fields": {
    "title": "Mathematics",
    "slug": "mathematics"
  }
}
]

dumpdata命令从数据库中转储数据到标准输出,默认用JSON序列化。返回的数据结构包括模型和它的字段信息,Django可以把它加载到数据库中。

你可以给这个命令提供应用的名称,或者用app.Model格式指定输出数据的模型。你还可以使用--format标签指定格式。默认情况下,dumpdata输出序列化的数据到标准输出。但是,你可以使用--output标签指定一个输出文件。--indent标签允许你指定缩进。关于更多dumpdata的参数信息,请执行python manage.py dumpdata --help命令。

使用以下命令,把这个转储保存到courses应用的fixtures/目录中:

mkdir courses/fixtures
python manage.py dumpdata courses --indent=2 --output=courses/fixtures/subjects.json

使用管理站点移除你创建的主题。然后使用以下命令把fixture加载到数据库中:

python manage.py loaddata subjects.json

fixture中包括的所有Subject对象已经加载到数据库中。

默认情况下,Django在每个应用的fixtures/目录中查找文件,但你也可以为loaddata命令指定fixture文件的完整路径。你还可以使用FIXTURE_DIRS设置告诉Django查找fixtures的额外目录。

Fixtures不仅对初始数据有用,还可以为应用提供简单的数据,或者测试必需的数据。

你可以在这里阅读如何在测试中使用fixtures。

如果你想在模型迁移中加载fixtures,请阅读Django文档的数据迁移部分。记住,我们在第九章创建了自定义迁移,用于修改模型后迁移已存在的数据。你可以在这里阅读数据库迁移的文档。

10.3 为不同的内容创建模型

我们计划在课程模型中添加不同类型的内容,比如文本,图片,文件和视频。我们需要一个通用的数据模型,允许我们存储不同的内容。在第六章中,我们已经学习了使用通用关系创建指向任何模型对象的外键。我们将创建一个Content模型表示单元内容,并定义一个通过关系,关联到任何类型的内容。

编辑courses应用的models.py文件,并添加以下导入:

from django.contrib.contenttypes.models import ContentType
from django.contrib.contenttypes.fields import GenericForeignKey

然后在文件结尾添加以下代码:

class Content(models.Model):
    module = models.ForeignKey(Module, related_name='contents')
    content_type = models.ForeignKey(ContentType)
    object_id = models.PositiveIntegerField()
    item = GenericForeignKey('content_type', 'object_id')

这是Content模型。一个单元包括多个内容,所以我们定义了一个指向Module模型的外键。我们还建立了一个通用关系,从代表不同内容类型的不同模型关联到对象。记住,我们需要三个不同字段来设置一个通用关系。在Content模型中,它们分别是:

  • content_type:一个指向ContentType模型的ForeignKey字段。
  • object_id:这是一个PositiveIntegerField,存储关联对象的主键。
  • item:通过组合上面两个字段,指向关联对象的GenericForeignKey字段。

在这个模型的数据库表中,只有content_typeobject_id字段有对应的列。item字段允许你直接检索或设置关联对象,它的功能建立在另外两个字段之上。

我们将为每种内容类型使用不同的模型。我们的内容模型会有通用字段,但它们存储的实际内容会不同。

10.3.1 使用模型继承

Django支持模型继承,类似Python中标准类的继承。Django为使用模型继承提供了以下三个选择:

  • 抽象模型:当你想把一些通用信息放在几个模型时很有用。不会为抽象模型创建数据库表。
  • 多表模型继承:可用于层次中每个模型本身被认为是一个完整模型的情况下。为每个模型创建一张数据库表。
  • 代理模型:当你需要修改一个模型的行为时很有用。例如,包括额外的方法,修改默认管理器,或者使用不同的元选项。不会为代理模型创建数据库表。

让我们近一步了解它们。

10.3.1.1 抽象模型

一个抽象模型是一个基类,其中定义了你想在所有子模型中包括的字段。Django不会为抽象模型创建任何数据库表。会为每个子模型创建一张数据库表,其中包括从抽象类继承的字段,和子模型中定义的字段。

要标记一个抽象模型,你需要在它的Meta类中包括abstract=True。Django会认为它是一个抽象模型,并且不会为它创建数据库表。要创建子模型,你只需要从抽象模型继承。以下是一个Content抽象模型和Text子模型的例子:

from django.db import models

class BaseContent(models.Model):
    title = models.CharField(max_length=200)
    created = models.DateTimeField(auto_now_add=True)
    
    class Meta:
        abstract = True

class Text(BaseContent):
    body = models.TextField()

在这个例子中,Django只会为Text模型创建数据库表,其中包括titlecreatedbody字段。

10.3.1.2 多表模型继承

在多表继承中,每个模型都有一张相应的数据库表。Django会在子模型中创建指向父模型的OneToOneField字段。

要使用多表继承,你必须从已存在模型中继承。Django会为原模型和子模型创建数据库表。下面是一个多表继承的例子:

from django.db import models

class BaseContent(models.Model):
    title = models.CharField(max_length=100)
    created = models.DateTimeField(auto_now_add=True)
    
class Text(BaseContent):
    body = models.TextField()

Django会在Text模型中包括一个自动生成的OneToOneField字段,并为每个模型创建一张数据库表。

10.3.1.3 代理模型

代理模型用于修改模型的行为,比如包括额外的方法或者不同的元选项。这两个模型都在原模型的数据库表上进行操作。在模型的Meta类中添加proxy=True来创建代理模型。

下面这个例子展示了如何创建一个代理模型:

from django.db import models
from django.utils import timezone

class BaseContent(models.Model):
    title = models.CharField(max_length=100)
    created = models.DateTimeField(auto_now_add=True)
    
class OrderedContent(BaseContent):
    class Meta:
        proxy = True
        ordering = ['created']
        
    def create_delta(self):
        return timezone.now() - self.created

我们在这里定义了一个OrderedContent模型,它是Content模型的代理模型。这个模型为QuerySet提供了默认排序和一个额外的created_delta()方法。ContentOrderedContent模型都在同一张数据库表上操作,并且可以用ORM通过任何一个模型访问对象。

10.3.2 创建内容模型

courses应用的Content模型包含一个通用关系来关联不同的内容类型。我们将为每种内容模型创建不用的模型。所有内容模型会有一些通用的字段,和一些额外字段存储自定义数据。我们将创建一个抽象模型,它会为所有内容模型提供通用字段。

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

class ItemBase(models.Model):
    owner = models.ForeignKey(User, related_name='%(class)s_related')
    title = models.CharField(max_length=250)
    created = models.DateTimeField(auto_now_add=True)
    updated = models.DateTimeField(auto_now=True)

    class Meta:
        abstract = True

    def __str__(self):
        return self.title

class Text(ItemBase):
    content = models.TextField()

class File(ItemBase):
    file = models.FileField(upload_to='files')

class Image(ItemBase):
    file = models.FileField(upload_to='images')

class Video(ItemBase):
    url = models.URLField()

在这段代码中,我们定义了一个ItemBase抽象模型。因此我们在Meta类中设置了abstract=True。在这个模型中,我们定义了ownertitlecreatedupdated字段。这些通用字段会用于所有内容类型。owner字段允许我们存储哪个用户创建了内容。因为这个字段在抽象类中定义,所以每个子模型需要不同的related_name。Django允许我们在related_name属性中为模型的类名指定占位符,比如%(class)s。这样,每个子模型的related_name会自动生成。因为我们使用%(class)s_related作为related_name,所以每个子模型对应的反向关系是text_relatedfile_relatedimage_relatedvideo_related

我们定义了四个从ItemBase抽象模型继承的内容模型。分别是:

  • Text:存储文本内容。
  • File:存储文件,比如PDF。
  • Image:存储图片文件。
  • Video:存储视频。我们使用URLField字段来提供一个视频的URL,从而可以嵌入视频。

除了自身的字段,每个子模型还包括ItemBase类中定义的字段。会为TextFileImageVideo模型创建对应的数据库表。因为ItemBase是一个抽象模型,所以它不会关联到数据库表。

编辑你之前创建的Content模型,修改它的content_type字段:

content_type = models.ForeignKey(
    ContentType,
    limit_choices_to = {
        'model__in': ('text', 'video', 'image', 'file')
    }
)

我们添加了limit_choices_to参数来限制ContentType对象可用于的通用关系。我们使用了model__in字段查找,来过滤ContentType对象的model属性为textvideoimage或者file

让我们创建包括新模型的数据库迁移。在命令行中执行以下命令:

python manage.py makemigrations

你会看到以下输出:

Migrations for 'courses':
  courses/migrations/0002_content_file_image_text_video.py
    - Create model Content
    - Create model File
    - Create model Image
    - Create model Text
    - Create model Video

然后执行以下命令应用新的数据库迁移:

python manage.py migrate

你看到的输出的结尾是:

Running migrations:
  Applying courses.0002_content_file_image_text_video... OK

我们已经创建了模型,可以添加不同内容到课程单元中。但是我们的模型仍然缺少了一些东西。课程单元和内容应用遵循特定的顺序。我们需要一个字段对它们进行排序。

10.4 创建自定义模板字段

Django自带一组完整的模块字段,你可以用它们构建自己的模型。但是,你也可以创建自己的模型字段来存储自定义数据,或者修改已存在字段的行为。

我们需要一个字段指定对象的顺序。如果你想用Django提供的字段,用一种简单的方式实现这个功能,你可能会想在模型中添加一个PositiveIntegerField。这是一个好的开始。我们可以创建一个从PositiveIntegerField继承的自定义字段,并提供额外的方法。

我们会在排序字段中添加以下两个功能:

  • 没有提供特定序号时,自动分配一个序号。如果存储对象时没有提供序号,我们的字段会基于最后一个已存在的排序对象,自动分配下一个序号。如果两个对象的序号分别是1和2,保存第三个对象时,如果没有给定特定序号,我们应该自动分配为序号3。
  • 相对于其它字段排序对象。课程单元将会相对于它们所属的课程排序,而模块内容会相对于它们所属的单元排序。

courses应用目录中创建一个fields.py文件,并添加以下代码:

from django.db import models
from django.core.exceptions import ObjectDoesNotExist

class OrderField(models.PositiveIntegerField):
    def __init__(self, for_fields=None, *args, **kwargs):
        self.for_fields = for_fields
        super().__init__(*args, **kwargs)

    def pre_save(self, model_instance, add):
        if getattr(model_instance, self.attname) is None:
            # no current value
            try:
                qs = self.model.objects.all()
                if self.for_fields:
                    # filter by objects with the same field values
                    # for the fields in "for_fields"
                    query = {field: getattr(model_instance, field) for field in self.for_fields}
                    qs = qs.filter(**query)
                # get the order of the last item
                last_item = qs.latest(self.attname)
                value = last_item.order + 1
            except ObjectDoesNotExist:
                value = 0
            setattr(model_instance, self.attname, value)
            return value
        else:
            return super().pre_save(model_instance, add)

这是我们自定义的OrderField。它从Django提供的PositiveIntegerField字段继承。我们的OrderField字段有一个可选的for_fields参数,允许我们指定序号相对于哪些字段计算。

我们的字段覆写了PositiveIntegerField字段的pre_save()方法,它会在该字段保存到数据库中之前执行。我们在这个方法中执行以下操作:

  1. 我们检查模型实例中是否已经存在这个字段的值。我们使用self.attname,这是模型中指定的这个字段的属性名。如果属性的值不是None,我们如下计算序号:
  • 我们构建一个QuerySet检索这个字段模型所有对象。我们通过访问self.model检索字段所属的模型类。
  • 我们用定义在字段的for_fields参数中的模型字段(如果有的话)的当前值过滤QuerySet。这样,我们就能相对于给定字段计算序号。
  • 我们用last_item = qs.lastest(self.attname)从数据库中检索序号最大的对象。如果没有找到对象,我们假设它是第一个对象,并分配序号0。
  • 如果找到一个对象,我们在找到的最大序号上加1。
  • 我们用setattr()把计算的序号分配给模型实例中的字段值,并返回这个值。
  1. 如果模型实例有当前字段的值,则什么都不做。

当你创建自定义模型字段时,让它们是通用的。避免分局特定模型或字段硬编码数据。你的字段应该可以用于所有模型。

你可以在这里阅读更多关于编写自定义模型字段的信息。

让我们在模型中添加新字段。编辑courses应用的models.py文件,并导入新的字段:

from .fields import OrderField

然后在Module模型中添加OrderField字段:

order = OrderField(blank=True, for_fields=['course'])

我们命名新字段为order,并通过设置for_fields=['course'],指定相对于课程计算序号。这意味着一个新单元会分配给同一个Course对象中最新的单元加1。现在编辑Module模型的__str__()方法,并如下引入它的序号:

def __str__(self):
    return '{}. {}'.format(self.order, self.title)

单元内容也需要遵循特定序号。在Content模型中添加一个OrderField字段:

order = OrderField(blank=True, for_fields=['module'])

这次我们指定序号相对于module字段计算。最后,让我们为两个模型添加默认排序。在ModuleContent模型中添加以下Meta类:

class Meta:
    ordering = ['order']

现在ModuleContent模型看起来是这样的:

class Module(models.Model):
    course = models.ForeignKey(Course, related_name='modules')
    title = models.CharField(max_length=200)
    description = models.TextField(blank=True)
    order = OrderField(blank=True, for_fields=['course'])

    class Meta:
        ordering = ['order']

    def __str__(self):
        return '{}. {}'.format(self.order, self.title)

class Content(models.Model):
    module = models.ForeignKey(Module, related_name='contents')
    content_type = models.ForeignKey(
        ContentType,
        limit_choices_to = {
            'model__in': ('text', 'video', 'image', 'file')
        }
    )
    object_id = models.PositiveIntegerField()
    item = GenericForeignKey('content_type', 'object_id')
    order = OrderField(blank=True, for_fields=['module'])

    class Meta:
        ordering = ['order']

让我们创建反映新序号字段的模型迁移。打开终端,并执行以下命令:

python manage.py makemigrations courses

你会看到以下输出:

You are trying to add a non-nullable field 'order' to content without a default; we can't do that (the database needs something to populate existing rows).
Please select a fix:
 1) Provide a one-off default now (will be set on all existing rows with a null value for this column)
 2) Quit, and let me add a default in models.py
Select an option:

Django告诉我们,因为我们在已存在的模型中添加了新字段,所以必须为数据库中已存在的行提供默认值。如果字段有null=True,则可以接受空值,并且Django创建迁移时不要求提供默认值。我们可以指定一个默认值,或者取消数据库迁移,并在创建迁移之前在models.py文件的order字段中添加default属性。

输入1,然后按下Enter,为已存在的记录提供一个默认值。你会看到以下输出:

Please enter the default value now, as valid Python
The datetime and django.utils.timezone modules are available, so you can do e.g. timezone.now
Type 'exit' to exit this prompt
>>>

输入0作为已存在记录的默认值,然后按下Enter。Django还会要求你为Module模型提供默认值。选择第一个选项,然后再次输入0作为默认值。最后,你会看到类似这样的输出:

Migrations for 'courses':
  courses/migrations/0003_auto_20170518_0743.py
    - Change Meta options on content
    - Change Meta options on module
    - Add field order to content
    - Add field order to module

然后执行以下命令应用新的数据库迁移:

python manage.py migrate

这个命令的输出会告诉你迁移已经应用成功:

Applying courses.0003_auto_20170518_0743... OK

让我们测试新字段。使用python manage.py shell命令打开终端,并如下创建一个新课程:

>>> from django.contrib.auth.models import User
>>> from courses.models import Subject, Course, Module
>>> user = User.objects.latest('id')
>>> subject = Subject.objects.latest('id')
>>> c1 = Course.objects.create(subject=subject, owner=user, title='Course 1', slug='course1')

我们已经在数据库中创建了一个课程。现在,让我们添加一些单元到课程中,并查看单元序号是如何自动计算的。我们创建一个初始单元,并检查它的序号:

>>> m1 = Module.objects.create(course=c1, title='Module 1')
>>> m1.order
0

OrderField设置它的值为0,因为这是给定课程的第一个Module对象。现在我们创建同一个课程的第二个单元:

>>> m2 = Module.objects.create(course=c1, title='Module 2')
>>> m2.order
1

OrderField在已存在对象的最大序号上加1来计算下一个序号。让我们指定一个特定序号来创建第三个单元:

>>> m3 = Module.objects.create(course=c1, title='Module 3', order=5)
>>> m3.order
5

如果我们指定了自定义序号,则OrderField字段不会介入,并且使用给定的order值。

让我们添加第四个单元:

>>> m4 = Module.objects.create(course=c1, title='Module 4')
>>> m4.order
6

这个单元的序号已经自动设置了。我们的OrderField字段不能保证连续的序号。但是它关注已存在的序号值,总是根据已存在的最大序号值分配下一个序号。

让我们创建第二个课程,并添加一个单元:

>>> c2 = Course.objects.create(subject=subject, owner=user, title='Course 2', slug='course2')
>>> m5 = Module.objects.create(course=c2, title='Module 1')
>>> m5.order
0

要计算新的单元序号,该字段只考虑属于同一个课程的已存在单元。因为这个第二个课程的第一个单元,所以序号为0。这是因为我们在Module模型的order字段中指定了for_fields=['course']

恭喜你!你已经成功的创建了第一个自定义模型字段。

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

推荐阅读更多精彩内容