实用性Mock的API接口---django框架(python)

一、背景介绍

  • 因在版本迭代过程中,一般都需要调用接口来实现需求业务。而前后端或各系统之间都存在强依赖性,故构思了此Mock接口;主要解决如下场景痛点:
  • 1、前后端的依赖关系:如前端已开发完毕但后端还没完成,导致前端无法进行调试;
  • 2、外部系统依赖关系:如外部系统未开发完或者环境的因素无法完成对接调试;
  • 3、测试阶段依赖关系:测试的某些场景无法模拟下,可调用mock接口设置自定义返回值,从而达到测试场景的覆盖(主要就是这块,因咱就是干测试滴);

二、构思设计及主要功能点

2.1)、主要功能介绍:

1、 接口可自定义规则:对mock接口依据配置规则,做字段数据必填项校验、数据类型校验
2、 接口可自定义匹配:可设置数据匹配规则,且支持设置多个匹配值;
3、 接口支持多级url、动态url、多种请求方式(get/post/put等)
4、 接口状态码自定义:可自定义设置接口返回状态码
5、 响应数据-可自定义变量返回:可依据python代码设置自定义变量(如时间戳、UUID等变量)
6、 接口可自定义控制关闭和开启,关闭后将无法调用
7、 响应数据后门属性:可依据请求指定key后,返回请求中的value数据

2.2)、功能/思路导图:

功能点及思路导图

三、直接先看最后的成果:

  • 1、Mock/路由配置页面:定义接口路径以及相应的规则.


    1.路由表截图--添加

    2.路由表截图--列表
  • 2、Mock/接口响应数据页面:定义接口匹配数据及返回的数据


    1.Mock响应数据截图--添加

    2.Mocke响应数据截图--列表
  • 3、实际调用结果:调用接口验证有效性.


    调用验证1:python调用

    调用验证2:接口测试页面使用

四、上代码说明( 因琐碎点太多,只抽取主流点和关键点作为说明)

  • 作为一个测试小白,比不了他们专业的开发人员。这小项目整整一个月断断续续的写完了,其中还是有很多优化空间,后续再慢慢调整;
  • 各位大佬,如有优化或其他构思思路欢迎指点评论,我再修改修改;


    代码目录结构
1、从开始的路由方面:
  • 1.1)、主路由:此处为入口的主路由,依据settings配置中ROOT_URLCONF字段判断主路由位置
#settings配置中ROOT_URLCONF字段判断主路由位置
ROOT_URLCONF = 'project.urls'
urlpatterns = [
    path(r'admin/mockapi/index/', views.IndexClass.index, name='index-test'),  # 接口测试页面
    path('admin/', admin.site.urls),#django后台管理页面路由
    path('api/mock/', include('mockapi.urls')),#通用接口的路由
    path('api/test/', include('testapi.urls')),#自定义接口的路由
]
urlpatterns += staticfiles_urlpatterns()
urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT)
  • 1.2)、二级路由(以api/mock为例):找到二级路由后并进入对应的views(视图)中,既进入视图逻辑:
urlpatterns = [
    path(r'index', IndexClass.index, name='index-test'),  # 接口测试页面
    path('caseid/registno', views.Caseid.as_view(), name='caseid'),
]
add_path = [
    re_path(r'(\w+)/(\w+)/(\w+)', views.CurrencyRoute.as_view(), name='test3-api-viewstwo'),  # 动态路由+3
    re_path(r'(\w+)/(\w+)', views.CurrencyRoute.as_view(), name='test2-api-viewstwo'),  # 动态路由+2
    re_path(r'(\w+)', views.CurrencyRoute.as_view(), name='test1-api-viewstwo'),  # 动态路由+1
]
urlpatterns.extend(add_path)
2、从views视图方面:
  • \color{blue}{2.1)、通用接口功能下的逻辑处理}
class CurrencyRoute(views.APIView, Currency):
    '''通用模式下-API接口视图类'''
    def __init__(self):
        super(Currency, self).__init__()
        self.code = ['code', '-1']
        self.msg = ['msg']
        self.data = ['data', None]
        self.DataCheck = DataCheck(self.code, self.msg, self.data)
        self.response_data = {self.code[0]: self.code[1], self.msg[0]: None, self.data[0]: self.data[1]}
    # 根据url找路由表数据:路由id、条件匹配规则、字段要求规则
    def __getroute(self, args, UrlType, replace='/api/mock/', APIOnOff=True):
        # route = "/".join(list(args)) if re_path.find('-') == -1 else self.find_path("/", re_path, args)
        route = self.request.path.replace(replace, '')
        try:
            get_data = RouteTabMock.objects.get(CustomUrl=route, UrlType=UrlType, APIOnOff=APIOnOff)
        except:
            # 兼容动态url接口,目前只支持两级动态,样例:test/case/{variable}/{variable}
            route_url = [m.start() for m in re.finditer('/', route)]
            for count_i in range(1, len(route_url)):
                try:
                    get_data = RouteTabMock.objects.get(CustomUrl=route[:route_url[-count_i]] + r'/{}' * count_i,
                                                        UrlType=UrlType, APIOnOff=APIOnOff)
                    get_data.ConditionValue = [route[route_url[-count_i] + 1:]]
                except RouteTabMock.DoesNotExist:
                    if count_i == len(route_url) - 1:
                        raise '动态url匹配模式下,未匹配该url=' + route
                else:
                    break
        re_data = get_data.id, get_data.ConditionValue, get_data.APIRequired
        logger.info('路由id={},条件匹配={},必填及类型规则={}'.format(re_data[0], re_data[1], re_data[2]))
        return re_data
    # 接口请求处理,数据处理/规则处理
    def __requestprocess(self, path_a):
        logger.info(
            '{}\n请求类型={},地址={},报文={}'.format(80 * '=', self.request.method, self.request.path, self.request.data))
                # 提取及处理request数据
        if self.request.method == 'GET':
            request_data = dict()
            for key, value in dict(self.request.GET).items():
                request_data[key] = value[0]
        else:
            request_data = dict(self.request.data)
        # 预留一个后门属性,如果有此test_data字段直接返回
        if Currency.test_data(request_data) != None:
            if self.request.method == 'GET':
                re_data = eval(request_data['test_data'])
            else:
                re_data = request_data['test_data']
            return re_data
        # 获取路由id、条件匹配规则、必填及数据类型规则
        try:
            re_dataid, conditionValue, apirequired = self.__getroute(path_a, UrlType=self.request.method)
        except:
            msg = '路由表未配置该url:' + self.request.path
            self.response_data[self.msg[0]] = msg
            logger.error('{},请求返回值={}'.format(msg, self.response_data))
            return self.response_data
        # 对传入参数进行必填项校验和数据类型的校验,校验不通过为False,返回Response错误的响应体
        re_data = self.DataCheck.check(eval(apirequired), request_data)
        if re_data != True:
            return re_data
        # 依据条件匹配规则,找到入参对应的条件查询值
        QueryValue = Querymethod().match_value(request_data, conditionValue, self.request.method)
        logger.info('条件匹配规则结果:' + str(QueryValue))
        # 如无条件匹配规则,取该接口下最新mock数据
        if QueryValue == False:
            try:
                getdb_data = APIResponseMock.objects.filter(CustomUrl_id=re_dataid)
                db_data = list(getdb_data.values())[-1]
                ResponseData, ResponseCode = self.re_variable(db_data['ResponseData']), db_data['ResponseCode']
                re_data = json.loads(ResponseData), ResponseCode
            except IndexError:
                msg = 'mock表未配置该url{}对应的数据'.format(self.request.path)
                self.response_data[self.msg[0]] = msg
                logger.error(msg)
        # 如果有条件匹配规则,对应的匹配值;如果存在多个匹配值,取第一个匹配的mock值
        else:
            try:
                con = self.Q_model(QueryValue, re_dataid)
                getdb_data = APIResponseMock.objects.filter(con)
                db_data = list(getdb_data.values())[-1]
                ResponseData, ResponseCode = self.re_variable(db_data['ResponseData']), db_data['ResponseCode']
                re_data = json.loads(ResponseData), ResponseCode
            except Exception as ex:
                msg = '未找到匹配值:' + str(QueryValue)
                re_data = {self.code[0]: self.code[1], self.msg[0]: msg, self.data[0]: self.data[1]}
                logger.error(msg)
        logger.info('请求返回值=' + str(re_data))
        return re_data
    # __接口状态码处理
    def __request(self, response_data):
        if type(response_data) == type(tuple()):
            if str(response_data[1]) in ('None', ''):
                response_data = response_data[0], 200
            re_data, status_code = response_data
        else:
            re_data, status_code = response_data, 200
        return re_data, status_code
    # post请求方式
    def post(self, request, *args):
        re_data, status_code = self.__request(self.__requestprocess(args))
        return Response(re_data, status=status_code)
    # Get请求方式
    def get(self, request, *args):
        re_data, status_code = self.__request(self.__requestprocess(args))
        return Response(re_data, status=status_code)
    # Put请求方式
    def put(self, request, *args):
        re_data, status_code = self.__request(self.__requestprocess(args))
        return Response(re_data, status=status_code)

*主要逻辑流介绍(不具体分解,详见上述代码内注释):
1、视图第一层处理:后门属性的判断,以及提取和返回处理
2、视图第二层处理:截取url后缀,得到url后找到路由表下对应的字段规则及匹配规则;
3、视图第三层处理:得到接口字段规则后,对路由进行必传和数据类型规则校验;
4、视图第四层处理:依据接口匹配规则,取入参值到mock数据表查找匹配值并响应数据;

  • \color{blue}{2.2)、视图第三层处理,必填项/数据类型校验处理:}
class DataCheck(object):
    '''数据必填+类型校验'''
    def __init__(self, code=['code', '-1'], msg=['msg'], data=['data', None]):
        self.key_list = []
        self.code = code
        self.msg = msg
        self.data = data
    # 校验必填项以及数据类型,如果不匹配抛出异常
    def __value_type(self, dict_b, type_a):
        dict_data = dict_b
        try:
            for keys in self.key_list:
                dict_data = dict_data[keys]
            if type(dict_data) != type(type_a) and type_a != True:
                raise ValueError('[{}]字段类型校验失败,期望传入类型为:{}'.format(keys, type(type_a)))
        except KeyError:
            if type_a == True:
                raise ValueError('必填校验失败,缺少必要参数:' + str(keys))
    def check(self, check_a, dict_b):
        try:
            # 为空不做校验,与页面规则一致
            if check_a == {}:
                return True
            re_data = self.get_dict_allkeys(check_a, dict_b)
        except Exception as ex:
            # 校验检查不通过,返回的报文;可通过类属性修改自定义key
            ex = str(ex).strip('()')
            re_data = {self.code[0]: self.code[1], self.msg[0]: ex, self.data[0]: self.data[1]}
            logger.error('入参不符合校验规则={},请求返回值={}'.format(check_a, re_data))
        return re_data
    # 无限遍历dict所有key,递归方式调用
    def get_dict_allkeys(self, check_a, dict_b):
        # 使用isinstance检测数据类型,dict递归
        if isinstance(check_a, dict):
            for x in range(len(check_a)):
                temp_key = list(check_a.keys())[x]
                temp_value = check_a[temp_key]
                if type(temp_value) in (type({}), type([])):
                    self.key_list.append(temp_key)
                self.get_dict_allkeys(temp_value, dict_b)  # 递归遍历
            # 回退dict的上一层结构
            self.key_list = self.key_list[:-1]
        elif isinstance(check_a, list):
            for key_a in check_a:
                if isinstance(key_a, dict):
                    # for x in range(len(key_a)):
                    # 递归到列表+元素位置0,即只支持列表第一个作为校验点
                    temp_key = list(key_a.keys())[0]
                    temp_value = key_a[temp_key]
                    self.key_list.append(0)
                    self.key_list.append(temp_key)
                    self.get_dict_allkeys(temp_value, dict_b)
                # 必填校验,兼容true,True
                if str(key_a) in ('true', 'True'):
                    self.__value_type(dict_b, True)
                    continue
                # 数据类型校验,目前校验str/int/bool(兼容缩写及全拼)
                if str(key_a) in ('str', 'string', "<class 'str'>"):
                    self.__value_type(dict_b, str())
                elif str(key_a) in ('int', 'integer', "<class 'int'>"):
                    self.__value_type(dict_b, int())
                elif str(key_a) in ('bool', 'boolean', "<class 'bool'>"):
                    self.__value_type(dict_b, bool())
            # 回退dict的上一层结构
            self.key_list = self.key_list[:-1]
        return True

*主要逻辑处理介绍:
1、无限遍历,如为dict既递归,如为list且再找到必填和数据类型的输入枚举值既做规则校验处理
2、如果不符合校验规则,既响应对应的错误信息.
*备注:说实话,这块的逻辑太复杂了,主要场景太多了;自己测的时候改bug改到崩溃,思路进入了死循环。也体会了下开发改bug的痛苦了;

  • \color{blue}{-2.3)、视图第四层处理,匹配规则的处理:}
class Querymethod(object):

    def match_value(self, databody, conditionValue, req_type):
        '''
        依据接口设置匹配规则,获取请求参数中的value;以此作为查询条件
        :param databody: 接口请求参数数据-type=dict()
        :param conditionValue: 接口配置的匹配规则条件-type=dict()
        :return: 根据匹配规则,返回请求体中value
        '''
        # 递归引用列表取key,直至到value为'True', 'true'结束
        def key_list(key_data, lista=list()):
            for keys, values in key_data.items():
                if type(values) == type({}) or str(values) in ('True', 'true'):
                    lista.append(keys)
                    True if str(values) in ('True', 'true') else key_list(values)
                elif type(values) == type([]):
                    lista.append(keys)
                    lista.append(0)
                    key_list(values[0], lista)
                else:
                    if lista == []:
                        lista.append(keys)
                    break
            return lista
        # 依据分号切割规则数据,再依次根据规则匹配databody对应值
        if type(conditionValue) == type([]):
            return conditionValue
        elif str(conditionValue).replace(' ', '') != '{}':
            re_listdata = []
            for key_data in str(conditionValue).split(";"):
                re_data = databody
                try:
                    lista = key_list(eval(key_data))
                    for value in lista:
                        re_data = re_data[value]
                except  KeyError:
                    if len(str(conditionValue).split(";")) == 1:
                        return False
                    lista.clear()
                else:
                    re_listdata.append(re_data)
                    lista.clear()
            re_data = re_listdata
        else:
            logger.warning('未找到到匹配的数据值,匹配规则=' + str(conditionValue))
            re_data = False
        return re_data

*主要逻辑处理介绍:
1、先判断是否空字典“{}”,如是返回False,既后续其他逻辑返回最新的一条数据;
2、依据分号" ; "切割,依据接口定义的匹配规则,依次找出入参中相应的值;
2、匹配值以列表形式存储,多个匹配值情况后续逻辑将依据正序条件命中并响应对应的数据;

3、从models模型方面:
  • \color{blue}{3.1)、RouteTabMock路由配置表:}
class RouteTabMock(models.Model):
    """Mock-路由URL表模型  """
    re_type = [("POST", "Post"), ("GET", "Get"), ("PUT", "Put")]
    APIOnOff = models.BooleanField(default=True, verbose_name='接口启用状态')
    UrlType = models.CharField(max_length=6, choices=re_type, default="Post", verbose_name='请求方式')
    UrlName = models.CharField(max_length=20, db_index=True, unique=True, verbose_name='接口名称')
    CustomUrl = models.CharField(max_length=50, db_index=True, unique=True, verbose_name='url地址')
    UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间')
    ConditionValue = models.TextField(default='{}', verbose_name='条件匹配规则')
    APIRequired = models.TextField(default='{}', verbose_name='字段要求规则')
    Remarks = models.CharField(max_length=200, null=True, blank=True, default='-', verbose_name='备注说明')
    class Meta:
        verbose_name = 'Mock/路由配置表'
        verbose_name_plural = 'Mock/路由配置表'
    # json格式校验,避免输入错误格式:字段要求规则、条件匹配规则
    def remarksdisplay(self):
        re_data = models_fun().list_setting(self.Remarks, intdata=20)
        return re_data
    remarksdisplay.allow_tage = True
    remarksdisplay.short_description = '备注说明'
    def __str__(self):
        return '{}:[{}]'.format(self.UrlName, self.CustomUrl)
DB-路由配置表

*主要逻辑处理介绍:

  • 1、本次数据存储引用django默认配置的sqlite3方式,为文件形式存储;主要是轻量级、且便捷,如后期mock数据量过大时,可替换专业的数据库(mysql或oracle这些,修改setting中DATABASES字段即可)
  • 2、各字段的设计、定义,class Meta为后端显示的名称(也可自定义表名,不添加DB的表名:既为子项目名称+类型);
  • 3、clean既对页面添加路由时,对字段要求规则及条件匹配规则做json格式校验;
  • 4、apirequired是解决后台列表字段内容超长时,列表高度被拉长情况(调用后,当输入内容超出设置值时,超长部分将显示"....";详解如下图)


    apirequired调用的models_fun().list_setting方法
  • \color{blue}{3.2)、接口响应数据表说明:}
class APIResponseMock(models.Model):
    """Mock-接口数据响应表模型 """
    CustomUrl = models.ForeignKey(RouteTabMock, on_delete=models.PROTECT, verbose_name='关联接口')
    UpdateTime = models.DateTimeField(auto_now=True, verbose_name='更新时间')
    QueryValue = models.CharField(max_length=50, db_index=True, verbose_name='查询值')
    ResponseData = models.TextField(verbose_name='响应信息数据')
    ResponseCode = models.PositiveIntegerField(null=True, blank=True, default=200, verbose_name='接口状态码')
    Remarks = models.CharField(max_length=200, null=True, blank=True, default='-', verbose_name='备注说明')
    def clean(self):
        try:
            json.loads(self.ResponseData)
        except json.JSONDecodeError:
            #兼容~{variable}变量形式写入,即转为空字典在做json检查
            try:
                str_data = re.findall(r"~{(.+?)}", self.ResponseData)
                replace_data = self.ResponseData.replace("~{" + str_data[0] + "}", "{}")
                json.loads(replace_data)
            except IndexError:
                raise ValidationError('输入的json数据变量不符合规则,请检查')
            except json.JSONDecodeError:
                raise ValidationError('输入的json数据不合法,请检查')
    class Meta:
        unique_together = ('CustomUrl', 'QueryValue',)
        verbose_name = 'Mock/接口响应数据表'
        verbose_name_plural = 'Mock/接口响应数据表'
    def reprofile(self):
        re_data = models_fun().list_setting(self.ResponseData)
        return re_data
    reprofile.allow_tage = True
    reprofile.short_description = '响应信息数据'
mock数据表

*主要逻辑处理介绍:

  • 1、如上一个节点的路由配置表同理,相同的逻辑;就不再单独说明了;
4、admin后台管理页面设置方面(以路由表举例):
class RouteTabMockAdmin(admin.ModelAdmin):
    def time_seconds(self, obj):
        return obj.UpdateTime.strftime("%Y-%m-%d %H:%M:%S")
    # 时间格式化处理
    time_seconds.admin_order_field = 'UpdateTime'
    time_seconds.short_description = '更新时间'
    list_display = ['id', 'UrlName', 'CustomUrl', 'UrlType', 'apirequired', 'routeprofile', 'time_seconds',
                    'remarksdisplay',
                    'APIOnOff']
    list_filter = ['CustomUrl']
    search_fields = ['CustomUrl', 'UrlName']
    list_per_page = 15  # 分页展示
    actions_on_top = False  # 页面删除触发指令
    actions_on_bottom = True  # 页面Action触发指令
    fieldsets = [
        ("接口信息", {"fields": ['UrlName', 'CustomUrl', 'UrlType']}),
        ("接口规则", {"fields": ['APIOnOff', 'APIRequired', 'ConditionValue', 'Remarks']})
    ]
admin.site.register(RouteTabMock, RouteTabMockAdmin)

五、日志模块方面:

class Log(object):
    level_relations = { 'debug': logging.DEBUG, 'info': logging.INFO, 'warning': logging.WARNING, 'error': logging.ERROR, 'crit': logging.CRITICAL}  # 日志级别关系映射
    def __init__(self, filename, level='info', when='D', backCount=3,
                 fmt='%(asctime)s - %(filename)s[line:%(lineno)d] - %(levelname)s: %(message)s'):
        self.logger = logging.getLogger(filename)
        format_str = logging.Formatter(fmt)  # 设置日志格式
        self.logger.setLevel(self.level_relations.get(level))  # 设置日志级别
        sh = logging.StreamHandler()  # 往屏幕上输出
        sh.setFormatter(format_str)  # 设置屏幕上显示的格式
        th = handlers.TimedRotatingFileHandler(filename=filename, when=when, backupCount=backCount,
                                               encoding='utf-8')  # 往文件里写入#指定间隔时间自动生成文件的处理器
        # 实例化TimedRotatingFileHandler
        # interval是时间间隔,backupCount是备份文件的个数,如果超过这个个数,就会自动删除,when是间隔的时间单位,单位有以下几种:
        # S 秒\M 分\H 小时\D 天\W 每星期(interval==0时代表星期一)\midnight 每天凌晨
        th.setFormatter(format_str)  # 设置文件里写入的格式
        self.logger.addHandler(sh)  # 把对象加到logger里
        self.logger.addHandler(th)
def create_file():
    filepath = os.path.realpath(__file__)
    output_dir = os.path.abspath(os.path.join(filepath, "../../logs"))
    if not os.path.exists(output_dir):
        os.makedirs(output_dir)
    log_name = '{}.log'.format(time.strftime('%Y-%m-%d'))
    filename = os.path.join(output_dir, log_name)
    return filename
filename = create_file()
log = Log(filename, level='debug')
logger = log.logger
log日志内容图片

*主要逻辑处理介绍:
1、引用的话,直接logger.info('输入打印内容')或者其他等级的即可。简单实用,可收藏为敬;
2、此日志引用的是logging库,百度一下一大推;能力有限,写不出那么强大的。是直接复制过来改一改,达到我想要的效果。

**备注:其他测试接口页面逻辑流、自定义接口逻辑流、简易封装、及琐碎配置就不再单独列举了;内容太多,估计看都能看烦.

六、结束收尾语:

  • 1、这个小项目,写写停停倒腾了一个月也总算勉强的完成了;目前准备在实际测试项目上应用,将能满足一些测试场景的需求以及提高测试效率;
  • 2、因还有很多逻辑以及细节就不再单独拿出来说明了,具体都有对应的注释,可自行了解查看;
  • 3、目前构思或者代码中应该还有很多优化,会在下个版本进行优化;
  • 4、下次版本新增功能点:
    --- 1)、接口支持表单形式;
    --- 2)、接口请求类型支持:PUT、PATCH、DELETE;
    --- 3)、接口测试页面依据postman参考重新设计页面和功能;
    --- 4)、接口不满足规则时,返回的错误信息支持自定义结构
mock系统使用说明pdf链接: https://pan.baidu.com/s/1JV8O66WLJ4q8Oh8UdsC-_A?pwd=8888 
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,921评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,635评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,393评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,836评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,833评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,685评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,043评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,694评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,671评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,670评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,779评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,424评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,027评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,984评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,214评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,108评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,517评论 2 343

推荐阅读更多精彩内容