django channels打造客服聊天系统

    最近公司业务需求, 打算开发一款在线客服的功能提供给app使用, 本人一开始打算是使用第三方平台, 比如腾讯云的客服系统来打造的, 不过后来查找资料, 了解了下websocket之后, 发现实现起来并不困难. 在此之前, 我研究了微信网页版的聊天方式, 发现微信网页版聊天并不是基于websocket的, 还是使用了http请求发送post请求, 进行页面长轮询方式进行信息的交互的. 

    由于公司项目使用了Django框架进行开发的, django框架有一个channels库是支持websocket服务的, 所以在进行开发之前需要进行安装channels

    打开终端, 输入:

    pip install -U channels

    在django配置文件中安装channels应用.

    使用命令django-admin startapp chat创建一个子应用chat, 并在项目的应用的上级目录新建一个routing.py文件(与wsgi.py同级), 作为websocket服务的路由规则. 


    在routing.py中输入以下代码:

    其实routing.py文件和django的总urls.py文件的原理是一样的,  django服务在运行的时候, 如果接收到的是websocket请求, 就会跳到routing.py文件中websocket应用, 接收到http请求时会跳到urls中找http应用. 

     当然, 再此之前需要在配置文件中配置websocket所支持的asgi协议, 配置如下:   

    本次使用了redis数据库作为websocket的管道,  单点通信的信息以及组间的通信数据都缓存在了redis数据库中, 配置redis管道如下:

配置完这些之后运行manage.py, 如截图所示说明成功了

之后正式开始编写业务代码了, 首先需要在刚刚创建的应用chat中创建一个consumers.py文件, 作为channels的消费者, websocket的消息通信都在这个文件中执行. 

复制channels官方文档中(https://channels.readthedocs.io/en/latest/tutorial/part_3.html)的组间消息通信代码到这个文件中,  然后根据这个基础代码进行业务代码的调整.

    其里面使用了python的async库来支持异步操作, 其实现了connect, disconnect,receive,chat_message等方法, connect用来连接用户组, 也可以理解为开始连接进行websocket通信; disconnect正好时断开连接, 客户端通过手动的断开websocket通信会经过此方法; chat_message就是用来发送消息的, receive方法就是进行消息的接收. 

    在chat应用中创建未读消息模型类, 用来记录聊天记录, 在models.py文件中添加如下代码:

from django.db import models

from users.models import User

# Create your models here.

class ChatRecords(models.Model):

    '''在线客服聊天记录模型类'''

    sender = models.ForeignKey(User, on_delete=models.CASCADE, related_name='send_records', verbose_name='发送者', null=False)

    receiver = models.ForeignKey(User, on_delete=models.CASCADE, related_name='receive_records', verbose_name='接收者', null=False)

    message = models.TextField(verbose_name='聊天信息')

    create_time = models.DateTimeField(auto_now_add=True, verbose_name='创建时间')

    class Meta:

        db_table = "tb_chat_records"

        verbose_name = '客服聊天记录'

        verbose_name_plural = verbose_name

    def __str__(self):

        return self.id

    接下来执行数据库迁移命令

    python manage.py makemigrations 

    数据库中生成表命令

    python manage.py migrate

    在consumers.py文件中添加业务代码, 添加结果如下:

# chat/consumers.py

import re

from django.conf import settings

from fdfs_client.client import Fdfs_client

from channels.exceptions import *

from calendar import timegm

from django_redis import get_redis_connection

from base64 import b64decode

from rest_framework_jwt.authentication import jwt_decode_handler

from channels.db import database_sync_to_async

from channels.generic.websocket import AsyncWebsocketConsumer

import json

class ChatConsumer(AsyncWebsocketConsumer):

    async def connect(self):

        # 获取url中的参数

        query_string = self.scope['query_string'].decode()

        params = query_string.split('&')

        item = {}

        for param in params:

            item[param.split('=')[0]] = param.split('=')[1]

        token = item.get('token')

        self.user_group = 'chat_'

        if token:

            try:

                payload = jwt_decode_handler(token)

            except:

                raise DenyConnection("签证错误")

            user_id = payload['user_id']

            user = await self.get_user(id=user_id)

            last_login = payload.get('last_login')

            if last_login != timegm(user.last_login.utctimetuple()):

                raise DenyConnection("签证已过期")

        else:

            user = self.scope['user']

        if not user:

            raise DenyConnection("用户不存在")

        receiver_name = item.get('receiver')

        if not receiver_name:

            raise DenyConnection("接收者名称错误")

        receiver = await self.get_user(username=receiver_name)

        if not receiver:

            raise DenyConnection("接收者不存在")

        self.receiver = receiver

        self.user = user

        # 远程组

        self.receiver_group = 'chat_%s_%s' % (self.receiver.username, self.user.username)

        # 用户组

        self.user_group = 'chat_%s_%s' % (self.user.username, self.receiver.username)

        # Join room group

        await self.channel_layer.group_add(

            self.user_group,

            self.channel_name

        )

        await self.accept()

    async def disconnect(self, close_code):

        # Leave room group

        await self.channel_layer.group_discard(

            self.user_group,

            self.channel_name

        )

    # Receive message from WebSocket

    async def receive(self, text_data):

        text_data_json = json.loads(text_data)

        message = text_data_json['message']

        if message:

            # data:image/png;base64,i

            ret = re.findall('data:image/.*;base64,(.*)', message)

            if ret:

                user_pic_str = ret[0]

                image_src = await self.save_image_to_fdfs(user_pic_str)

                # 构造message

                message = '<img style="width: 80px; height: 60px" src="'+ image_src +'" data-preview-src="">'

            # Send message to room group

            chat_record = await self.save_model(self.user, self.receiver, message)

            if self.receiver.username == 'admin':

                '''为管理员添加消息提示'''

                await self.save_unread_records(chat_record, self.user)

            await self.channel_layer.group_send(

                # websocket发送消息

                self.receiver_group,

                {

                    'type': 'chat_message',

                    'message': message

                }

            )

    # Receive message from room group

    async def chat_message(self, event):

        message = event['message']

        # Send message to WebSocket

        await self.send(text_data=json.dumps({

            'message': message

        }))

    @database_sync_to_async

    def save_model(self, sender, receiver, message):

        # 保存消息记录到数据库中

        from .models import ChatRecords

        return ChatRecords.objects.create(sender=sender, receiver=receiver, message=message)

    @database_sync_to_async

    def get_user(self, id=None, username=None):

        # 异步获取用户

        from users.models import User

        user = None

        if id:

            try:

                user = User.objects.get(id=id)

            except:

                return None

        if username:

            try:

                user = User.objects.get(username=username)

            except:

                return None

        return user

    async def save_unread_records(self, chat_record, sender):

        # 保存未读消息

        redis_conn = get_redis_connection('chatRecord')

        p = redis_conn.pipeline()

        p.rpush(sender.id, chat_record.id)

        p.set('new_records', 1)  # 在redis中添加未读标记

        p.execute()

    async def save_image_to_fdfs(self, pic_str):

        # 把图片存储到fastdfs文件系统中

        client = Fdfs_client(settings.FDFS_CLIENT_CONF)

        ret = client.upload_appender_by_buffer(b64decode(pic_str))

        if ret.get("Status") != "Upload successed.":

            raise Exception("upload file failed")

        file_name = ret.get("Remote file_id")

        return settings.FDFS_URL + file_name

    接下来还需要在chat应用中创建一个routing.py文件, 将子路由会分发到这个文件中, 输入以下代码:

通过上面的步骤, django channels搭建websocket的任务已经完成, 这个时候需要为后台管理页面提供一个聊天界面,  在项目的模板目录中创建一个chat文件夹, 在里面创建一个index.html文件

复制以下代码进去

<!DOCTYPE html>

<html>

<head>

    <meta charset="UTF-8">

    <title>koalas 客服</title>

    <style type="text/css">

        body{

            background: url("http://47.75.156.19:8020/static/chat/images/bg.jpg") no-repeat ;

            background-size: cover;

        }

        * {

            margin: 0;

            padding: 0;

        }

        li,

        img,

        label,

        input {

            vertical-align: middle;

        }

        img {

            display: block;

            max-height: 100%;

            margin: 0;

            padding: 0;

        }

        a {

            text-decoration: none;

            color: black;

        }

        ul, li {

            list-style: none;

        }

        -webkit-scrollbar-track {

            background-color: transparent;

        }

        .defaultPage {

        / / 缺省页 width: 200 px;

            height: 300px;

            margin-top: 120px;

        }

        .defaultPage > .img-box {

            width: 120px;

            height: 120px;

            margin: 20px auto;

        }

        .defaultPage > .img-box > img {

            width: 100%;

            height: 100%;

        }

        .defaultPage > .noMsg {

            margin: 20px 0;

            text-align: center;

            font-size: 14px;

            color: rgba(153, 153, 153, 1);

        }

        .defaultPage > .helpButton {

            margin: 20px 40px;

            text-align: center;

            font-size: 14px;

            line-height: 42px;

            color: #fff;

            background-color: #000000;

        }

        .kefu {

            display: flex;

            margin: auto;

            border: 1px solid #333;

            position: absolute;

            top: 50px;

            left: 250px;

            right: 250px;

            bottom: 50px;

        }

        .left {

            width: 18%;

            overflow-x:hidden; overflow-y:auto;

            border-right: 1px solid #999;

            background-color: #333;

        }

        .left .list {

            height: 100%;

        }

        .left .list .list-item {

            display: flex;flex-wrap: nowrap;

            padding: 15px;overflow: hidden;

            border-bottom: 1px solid #999;

            color: #fff;

        }

        .cur{

            background-color: #333;

            opacity:0.6;

        }

        .left .list .list-item .img-box {

            position: relative;

            width: 36px; height: 36px;

            margin-right: 10px;

            border-radius: 4px;

        }

        .left .list .list-item .img-box img {

            width: 100%;height: 100%;

            display: inline-block;

            vertical-align: middle;

        }

        .left .list .list-item .bandage{

            display:inline-block ;

            position: absolute;right: -8px;top:-8px;

            width:16px;height: 16px;border-radius: 12px;

            text-align: center;

            font-size:12px;line-height: 16px;

            background-color: red;

        }

        .left .list .list-item .bandage span{

            color: #fff;

        }

        .left .list .list-item .info{

            flex: 1;padding-left: 10px;

        }

        .left .list .list-item .name {

            font-size: 14px;line-height: 22px;

        }

        .left .list .list-item .text{

            display: flex;flex-wrap: nowrap;

            font-size:12px;

            text-align: left;

        }

        .left .list .list-item .text span{

            flex:1;

        }

        .right {

            flex: 1;

            height: 100%;

            display: flex;

            flex-direction: column;

            background-color: #c3c3c3;

        }

        .right .name {

            position: relative;

            width: 100%;

            height: 50px;

            line-height: 50px;

            text-align: center;

            border-bottom: 1px solid #999;

        }

        .name .moreMsg{

            position: absolute; left:46%;top: 61px;

            height: 20px;

            padding: 0 6px;

            border-radius: 6px;

            background-color: #FFF;

            color: red;

            font-size:12px;line-height: 20px;

            z-index:98;

        }

        .right .content {

            flex:1;

            overflow-x:hidden; overflow-y:auto;

            display: flex;flex-direction: column;

            width: 100%;

            padding: 10px;

            box-sizing: border-box;

            z-index: 88;

        }

        .right .content .message{

            margin-top: 24px;

            flex: 1;

        }

        .right .send_msg {

            position: relative;

            height: 160px;

            overflow: hidden;

            padding: 10px;

            border-top: 1px solid #999;

            z-index: 2;

        }

        .right .send_msg .input_text {

            display: block;

            width: 100%;

            height: 60%;

            box-sizing: border-box;

            padding: 10px;

            background-color: #c3c3c3;

        }

        .right .send_msg .btn_send {

            position: absolute;

            bottom: 10px;

            right: 20px;

            width: 70px;

            height: 22px;

            margin-top: 100px;

            border-radius: 4px;

            font-size: 14px;

            line-height: 22px;

            text-align: center;

            border: 1px solid #333;

        }

        .content .time{

            display: flex;

            justify-content: center;

            color: #fff;

            line-height: 40px;

            z-index: 4;

        }

        .content .reply {

            display: flex;

            width: 80%;

            margin-bottom: 15px;

        }

        .content .reply .img-box, .content .user .img-box {

            display: inline-block;

            width: 40px;

            height: 40px;

        }

        .content .reply .img-box img, .content .user .img-box img {

            width: 100%;

            height: 100%;

        }

        .content .reply .text-cont {

            flex: 1;

            padding: 4px 0;

            margin-left: 15px;

        }

        .content .reply .text-cont .text, .content .user .text-cont .text {

            display: inline-block;

            padding: 4px 6px;

            font-size: 14px;

            line-height: 28px;

            border-radius: 4px;

            border: 1px solid #999;

            background-color: #fff;

        }

        .content .user .text-cont img{

            float: right;

        }

        .content .show {

            visibility: hidden;

        }

        .content .user {

            float: right;

            display: flex;

            width: 80%;

            margin-bottom: 15px;

        }

        .content .user .text-cont {

            flex: 1;

            text-align: right;

            padding: 4px;

            margin-right: 15px;

        }

        .content .user .text-cont .text {

            background-color: #b2e281;

        }

        .content .user .img-box {

            width: 40px;

            height: 40px;

        }

        .content .user .img-box img {

            width: 100%;

            height: 100%;

        }

        .bigimg{width:600px;position: fixed;left: 0;top: 0; right: 0;bottom: 0;margin:auto;display: none;z-index:9999;}

.mask{position: fixed;left: 0;top: 0; right: 0;bottom: 0;background-color: #000;opacity:0.5;filter: Alpha(opacity=50);z-index: 98;transition:all 1s;display: none}

    .bigbox{width:840px;background: #fff;border:1px solid #ededed;margin:0 auto;border-radius: 10px;overflow: hidden;padding:10px;}

    .bigbox>.text-cont{width:400px;height:250px;float:left;border-radius:5px;overflow: hidden;margin: 0 10px 10px 10px;}

    .bigbox>.imgbox>img{width:100%;}

        .text-cont:hover{cursor:zoom-in}

        .mask:hover{cursor:zoom-out}

        .mask>img{position: fixed;right:10px;top: 10px;width: 60px;}

        .mask>img:hover{cursor:pointer}

    </style>

</head>

<body>

<img class="bigimg">

<div class="mask">

    <img src="http://47.75.156.19:8020/static/chat/images/close.png" alt="">

</div>

<div class="kefu">

    <div class="left">

        <ul class="list">

            <li class="list-item" :class="{cur: iscur === idx}" @click="handleSelect(item);iscur=idx" v-for="(item,idx) in senderLis" :key="idx">

                <div class="img-box">

                    <img :src="[[ item.user_pic ]]">

                    <div class="bandage" v-if="item.count !== 0">

                        <span >[[item.count]]</span>

                    </div>

                </div>

                <div class="info">

                  <span class="name">[[ item.username ]]</span>

                    <div class="text" v-html="item.last_send_message">

                    </div>

                </div>

            </li>

        </ul>

    </div>

    <div class="right">

        <div class="name" v-if="!resultShow">

            <span>[[ name ]]</span>

            <div class="moreMsg" @click="getMoreMsg" v-if="next">

                <span>更多消息</span>

            </div>

        </div>

        <div class="defaultPage" v-if="resultShow">

            <div class="img-box">

                <img :src="defaultPage">

            </div>

            <div class="noMsg">

                <span>未选择聊天!</span>

            </div>

        </div>

        <div class="content" v-show="!resultShow">

            <div v-for="(item,idx) in chatList" :key="idx" class="message">

                    <div class="time">

                        <span>[[item.create_time]]</span>

                    </div>

                <div class="reply" v-if="item.sender_name !== 'admin'">

                    <div class="img-box">

                        <img :src="clientImg"/>

                    </div>

                    <div class="text-cont" v-html="item.message">

                    </div>

                </div>

                <div class="user" v-else>

                    <div class="text-cont" v-html="item.message">

                    </div>

                    <div class="img-box">

                        <img :src="userImg"/>

                    </div>

                </div>

            </div>

        </div>

        <div class="send_msg" v-show="!resultShow">

            <textarea id="dropBox" class="input_text" autofocus v-model.trim="value" @keyup.enter="sendMsg"></textarea>

            <div class="btn_send" @click="sendMsg">发送</div>

        </div>

    </div>

</div>

</body>

<script src="https://cdn.bootcss.com/vue/2.6.10/vue.min.js"></script>

<script src="https://cdn.bootcss.com/axios/0.19.0-beta.1/axios.min.js"></script>

<script src="https://cdn.bootcss.com/jquery/3.4.0/jquery.min.js"></script>

<script src="http://47.75.156.19:8020/static/chat/js/zoom.js"></script>

<script type="text/javascript">

    var obj;

    var global_host = "127.0.0.1:8000";

    axios.defaults.withCredentials=true;

    window.onbeforeunload= function(event) {

          var flag =  confirm("确定离开此页面吗?");

          if(flag==true){

              vm.chatSocket.close();

          }

        return flag

    };

    var vm = new Vue({

        el: '.kefu',

        delimiters: ['[[', ']]'], // 修改vue模板符号,防止与django冲突

        data: {

            'resultShow': true,

            'defaultPage': '',

            'clientImg': '',

            'userImg': 'http://47.75.156.19:8020/static/chat/images/user02.jpg',

            'name': "",

//            "senders": senders,

            'iscur': '',

            "chatList": "",

            "next": "",

            "value": '',

            'chatSocket': '',

            'senderLis': '',

            'current_id': '',

        },

        methods: {

            getCookie (name) {

              var value = '; ' + document.cookie;

              var parts = value.split('; ' + name + '=');

              if (parts.length === 2) return parts.pop().split(';').shift()

            },

            getData(timeout){

                var url = 'http://'+ global_host +'/api/chat/unread/records/';

                axios.post(url, {timeout:timeout}, {headers:{'X-CSRFToken': this.getCookie('csrftoken')}}).then(res =>{

                    this.senderLis = res.data;

                    setTimeout(this.getData(30),300)

                })

            },

            handleSelect(item){

                this.clientImg = item.user_pic;

                if(this.current_id === item.sender_id){

                    return

                }

                else {this.current_id = item.sender_id}

                if(this.chatSocket){

                    this.chatSocket.close()

                }

                this.chatSocket = new WebSocket('ws://'+ global_host +'/ws/chat/?receiver=' + item.username );

                $('.content').html('');

                this.chatList = '';

                this.resultShow = false;

                this.name = item.username;

                axios.get('http://'+ global_host +'/api/chat/records/' + item.sender_id + "/?page_size=10").then(res => {

                    var data = res.data;

                    this.chatList = data.results;

                    this.next = data.next;

                    this.$nextTick(function () {

                        obj = new zoom('mask', 'bigimg','text-cont img');

                        obj.init();

                        $('.text-cont img').click(function () {

                            $('.bigimg').attr('src', $(this).attr('src'))

                        })

                    })

                }).then(function () {

                    // 滚动到底部

                    var h = $('.content')[0].scrollHeight;

                    $('.content').scrollTop(h);

                });

                this.chatSocket.onmessage = function (e) {

                    // 接收消息

                    var data = JSON.parse(e.data);

                    // 发起请求删除redis中记录

                    axios({

                        method:'delete',

                        url:'http://'+ global_host +'/api/chat/records/' + item.sender_id + "/",

                        headers: {'X-CSRFToken': vm.getCookie('csrftoken')}

                    }).then().catch(error =>{

                        console.log(error.response.data)

                    });

                    var message = data['message'];

                    $('.content').append('<div class="message"><div class="reply"><div class="img-box"><img src="'+ vm.clientImg +'"></div><div class="text-cont" >'+ message +'</div></div></div>');

                    obj.init();

                    $('.text-cont img').click(function () {

                        $('.bigimg').attr('src', $(this).attr('src'))});

                    // 滚动到底部

                    var h = $('.content')[0].scrollHeight;

                    $('.content').scrollTop(h);

                };

                this.chatSocket.onclose = function (e) {

                    console.error('Chat socket closed unexpectedly');

                };

                // 发起请求删除redis中记录

                axios({

                    method:'delete',

                    url:'http://'+ global_host +'/api/chat/records/' + item.sender_id + "/",

                    headers: {'X-CSRFToken': this.getCookie('csrftoken')}

                }).then().catch(error =>{

                    console.log(error.response.data)

                })

            },

            getMoreMsg(){

                var url = this.next;

//                alert(url)

                axios.get(url).then(res =>{

                    var data = res.data;

                    this.next = data.next;

                    this.chatList = data.results.concat(this.chatList)

                })

            },

            sendMsg(){

                if(!this.value||this.value ===''){

                    alert("请输入消息");

                    return

                }

                this.chatSocket.send(JSON.stringify({

                'message': '<span class="text" >'+ this.value +'</span>'

                }));

                $('.content').append('<div class="message"><div class="user"><div class="text-cont"><span class="text">' + this.value + '</span></div> <div class="img-box"><img src="'+ this.userImg +'"></div></div></div>' );

                this.value = '';

                // 滚动到底部

                var h = $('.content')[0].scrollHeight;

                $('.content').scrollTop(h);

            }

        },

        created(){

            this.getData(0);

        },

        mounted(){

        }

    })

    var dropBox;

  window.onload=function(){

  dropBox = document.getElementById("dropBox");

  // 鼠标进入放置区时

  dropBox.ondragenter = ignoreDrag;

  // 拖动文件的鼠标指针位置放置区之上时发生

  dropBox.ondragover = ignoreDrag;

  dropBox.ondrop = drop;

  }

  function ignoreDrag(e){

  // 确保其他元素不会取得该事件

  e.stopPropagation();

  e.preventDefault();

  }

  function drop(e){

  e.stopPropagation();

  e.preventDefault();

  // 取得拖放进来的文件

  var data = e.dataTransfer;

  var files = data.files;

  // 将其传给真正的处理文件的函数

  var file = files[0];

  var reader = new FileReader();

  reader.onload=function(e){

    $('.content').append('<div class="message"><div class="user"><div class="text-cont"><img style="width: 80px; height: 60px;" src="'+e.target.result+'" data-preview-src=""></div> <div class="img-box"><img src="'+ vm.userImg +'"></div></div></div>' );

    obj.init();

    $('.text-cont img').click(function () {

        $('.bigimg').attr('src', $(this).attr('src'))});

  vm.chatSocket.send(JSON.stringify({

        'message': e.target.result

    }));

  };

  reader.readAsDataURL(file);

  }

</script>

</html>

之后为django Xadmin后台添加一个模块, 把后台聊天页面添加到xadmin中.

首先在xadmin全局配置中添加get_site_menu方法, 为聊天页面添加一个导航框, 代码如下:

之后进行注册全局配置类

from xadmin import views

xadmin.site.register(views.CommAdminView, GlobalSetting)

运行manage.py, 打开谷歌浏览器输入127.0.0.1:8000/xadmin, 打开截图如下:


多了一个chat模块, 点开进去, 进入聊天界面(小编的这个界面是模仿了微信界面), 怎么进入到了微信网页界面呢? 哈哈哈.

由于这个是app端和后台客服管理端的聊天应用, 所以要把本地代码部署到服务器上, 然后借助前端混合开发小哥的移动端才能进行消息通讯, 不过各位朋友也可以相应的制作一款网页版的聊天系统来玩玩.

下一期内容就是部署这个channels到服务器上, 敬请期待...

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

推荐阅读更多精彩内容

  • ¥开启¥ 【iAPP实现进入界面执行逐一显】 〖2017-08-25 15:22:14〗 《//首先开一个线程,因...
    小菜c阅读 6,358评论 0 17
  • H5移动端知识点总结 阅读目录 移动开发基本知识点 calc基本用法 box-sizing的理解及使用 理解dis...
    Mx勇阅读 4,395评论 0 26
  • 移动开发基本知识点 一.使用rem作为单位 html { font-size: 100px; } @media(m...
    横冲直撞666阅读 3,453评论 0 6
  • 今天去三姑家了,大姑家的几个女儿女婿来拜年,大家一起吃了个年饭。今年过节在老家没有去一家我的亲戚家,都是老...
    栋姐阅读 155评论 0 1
  • 图片上的是我的爸爸妈妈,爸爸抱着我的女儿,妈妈抱着姐姐的女儿,你们都是我的最爱,在这里我就先谢谢我的爸爸 1.谢谢...
    倾斜5度阅读 318评论 0 3