极光推送客户端和服务器端开发

现在大不分应用都有推送的功能,今天总结下最近用到的极光推动的android端和服务器端开发过程。

1.1推送的原理

分服务器和客户端分别进行说明:

1.推送框架图

jpush.png
  • 推送的数据源:自己开发的服务器端或者使用极光推送官网的WEB后台(++用于客户自定义服务器向推送框架进行推送++)
  • JPush API:部署在服务器端,开发者的服务器端发起推送时,将数据传到JPush API中,然后向下传递
  • 建立长链接:集成JPush的SDK客户端启动后会建立一个到JPush Cloud的长链接(++手机客户端收到推送消息++)

1.2客户端原理

客户端需要和服务器保存长连接状态。jpush SDK提供了TCP Long Connection。实现的原理:

  • 心跳:为了长时间保持外网IP,需要客户端定期发送心跳给运营商,以便刷新NAT列表
  • Timer定时方法:该类计划循环执行定时任务,但是使用该类会使CPU保持唤醒状态,比较费电。
  • AlarmManager定时方法:该类封装了Android手机的RTC硬件时钟模块,可以在CPU休眠时正常运行,保持任务执行时再唤醒CPU,这样做到了电量节省。

2.实现步骤

2.1注册极光推送

极光推送网址:https://www.jiguang.cn
进行自行注册,然后进入控制台,添加自己的应用。在应用设置中就能看到自己的AppKey的值,这个是此用于的唯一标示符,在客户端开发和服务器端开发中都会用到此值。

2.2集成android客户端

集成android.png

按照上图的提示进行炒作即可。
下面在android studio中介绍下集成流程:

  1. builde.gradle中添加引用,即导入依赖包
compile 'cn.jiguang.sdk:jpush:3.0.0'
compile 'cn.jiguang.sdk:jcore:1.0.0'

2.添加统计信息,也可以不使用

@Override
    protected void onResume() {
        super.onResume();
        MobclickAgent.onResume(this);
    }

    @Override
    protected void onPause() {
        super.onPause();
        MobclickAgent.onPause(this);
    }

3.jpush初始化

public class MyApplication extends Application {

    public static String APPID = "915a5566e71a88d33a2851e0d2b19512";

    @Override
    public void onCreate() {
        JPushInterface.setDebugMode(true);  // 设置开启日志,发布时请关闭日志
        JPushInterface.init(this);          // 初始化 JPush
    }

}

4.在AndroidMainfest.xml添加

    <!-- Required 极光推送添加 -->
    <permission
        android:name="com.ylink.MGessTraderYlink_gxjszx.permission.JPUSH_MESSAGE"
        android:protectionLevel="signature" />

    <uses-permission android:name="com.ylink.MGessTraderYlink_gxjszx.permission.JPUSH_MESSAGE" />
    <uses-permission android:name="android.permission.RECEIVE_USER_PRESENT" />
    <uses-permission android:name="android.permission.WAKE_LOCK" />
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.WRITE_SETTINGS" />
    <uses-permission android:name="android.permission.VIBRATE" />
    <uses-permission android:name="android.permission.MOUNT_UNMOUNT_FILESYSTEMS" />
    <uses-permission android:name="android.permission.INTERNET" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.CHANGE_CONFIGURATION" />
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses-permission android:name="android.permission.ACCESS_WIFI_STATE" />
 
  <!-- Required SDK核心功能 -->
        <activity
            android:name="cn.jpush.android.ui.PushActivity"
            android:configChanges="orientation|keyboardHidden"
            android:exported="false"
            android:theme="@android:style/Theme.NoTitleBar">
            <intent-filter>
                <action android:name="cn.jpush.android.ui.PushActivity" />

                <category android:name="android.intent.category.DEFAULT" />
                <category android:name="com.ylink.MGessTraderYlink_gxjszx" />
            </intent-filter>
        </activity>
        <!-- Required  SDK核心功能 -->
        <service
            android:name="cn.jpush.android.service.DownloadService"
            android:enabled="true"
            android:exported="false" />

        <!-- Required SDK 核心功能 -->
        <!-- 可配置android:process参数将PushService放在其他进程中 -->
        <service
            android:name="cn.jpush.android.service.PushService"
            android:enabled="true"
            android:exported="false">
            <intent-filter>
                <action android:name="cn.jpush.android.intent.REGISTER" />
                <action android:name="cn.jpush.android.intent.REPORT" />
                <action android:name="cn.jpush.android.intent.PushService" />
                <action android:name="cn.jpush.android.intent.PUSH_TIME" />
            </intent-filter>
        </service>
        <!-- Required SDK核心功能 -->
        <receiver
            android:name="cn.jpush.android.service.PushReceiver"
            android:enabled="true">
            <intent-filter android:priority="1000">
                <action android:name="cn.jpush.android.intent.NOTIFICATION_RECEIVED_PROXY" /> <!-- Required  显示通知栏 -->
                <category android:name="com.ylink.MGessTraderYlink_gxjszx" />
            </intent-filter>
            <intent-filter>
                <action android:name="android.intent.action.USER_PRESENT" />
                <action android:name="android.net.conn.CONNECTIVITY_CHANGE" />
            </intent-filter>
            <!-- Optional -->
            <intent-filter>
                <action android:name="android.intent.action.PACKAGE_ADDED" />
                <action android:name="android.intent.action.PACKAGE_REMOVED" />

                <data android:scheme="package" />
            </intent-filter>
        </receiver>

        <!-- Required SDK核心功能 -->
        <receiver
            android:name="cn.jpush.android.service.AlarmReceiver"
            android:exported="false" />
        <!-- User defined.  For test only  用户自定义的广播接收器 -->
        <receiver
            android:name=".MyReceiver"
            android:enabled="true"
            android:exported="false">
            <intent-filter>
                <action android:name="cn.jpush.android.intent.REGISTRATION" /> <!-- Required  用户注册SDK的intent -->
                <action android:name="cn.jpush.android.intent.MESSAGE_RECEIVED" /> <!-- Required  用户接收SDK消息的intent -->
                <action android:name="cn.jpush.android.intent.NOTIFICATION_RECEIVED" /> <!-- Required  用户接收SDK通知栏信息的intent -->
                <action android:name="cn.jpush.android.intent.NOTIFICATION_OPENED" /> <!-- Required  用户打开自定义通知栏的intent -->
                <action android:name="cn.jpush.android.intent.CONNECTION" /> <!-- 接收网络变化 连接/断开 since 1.6.3 -->
                <category android:name="com.ylink.MGessTraderYlink_gxjszx" />
            </intent-filter>
        </receiver>
        <!-- Required  . Enable it you can get statistics data with channel -->
        <meta-data
            android:name="JPUSH_CHANNEL"
            android:value="developer-default" />
        <meta-data
            android:name="JPUSH_APPKEY"
            android:value="yourappkey" />

5.自定义MyReceiver

/**
 * 自定义接收器
 *
 * 如果不定义这个 Receiver,则:
 * 1) 默认用户会打开主界面
 * 2) 接收不到自定义消息
 */
public class MyReceiver extends BroadcastReceiver {
    private NotificationManager notificationManager;
    private int reqCode = 0;
    private NotificationImpl notificationImpl;

    @Override
    public void onReceive(Context context, Intent intent) {
        if (notificationManager == null) {
            notificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE);
        }
        Bundle bundle = intent.getExtras();
        MyLog.i("[MyReceiver] onReceive - " + intent.getAction() + ", extras: " + printBundle(bundle));

        if (JPushInterface.ACTION_REGISTRATION_ID.equals(intent.getAction())) {
            String regId = bundle.getString(JPushInterface.EXTRA_REGISTRATION_ID);
            MyLog.i("[MyReceiver] 接收Registration Id : " + regId);

        } else if (JPushInterface.ACTION_MESSAGE_RECEIVED.equals(intent.getAction())) {
            MyLog.i("[MyReceiver] 接收到推送下来的自定义消息: " + bundle.getString(JPushInterface.EXTRA_MESSAGE));
            showNotification(context,bundle);

        } else if (JPushInterface.ACTION_NOTIFICATION_RECEIVED.equals(intent.getAction())) {
            MyLog.i("[MyReceiver] 接收到推送下来的通知");
            int notifactionId = bundle.getInt(JPushInterface.EXTRA_NOTIFICATION_ID);
            MyLog.i("[MyReceiver] 接收到推送下来的通知的ID: " + notifactionId);
            receivingNotification(context,bundle);


        } else if (JPushInterface.ACTION_NOTIFICATION_OPENED.equals(intent.getAction())) {
            MyLog.i("[MyReceiver] 用户点击打开了通知");
            openNotification(context,bundle);
        } else if (JPushInterface.ACTION_RICHPUSH_CALLBACK.equals(intent.getAction())) {
            MyLog.i("[MyReceiver] 用户收到到RICH PUSH CALLBACK: " + bundle.getString(JPushInterface.EXTRA_EXTRA));
            //在这里根据 JPushInterface.EXTRA_EXTRA 的内容处理代码,比如打开新的Activity, 打开一个网页等..

        } else if(JPushInterface.ACTION_CONNECTION_CHANGE.equals(intent.getAction())) {
            boolean connected = intent.getBooleanExtra(JPushInterface.EXTRA_CONNECTION_CHANGE, false);
            MyLog.i("[MyReceiver]" + intent.getAction() +" connected state change to "+connected);
        } else {
            MyLog.i("[MyReceiver] Unhandled intent - " + intent.getAction());
        }
    }

    //自定义通知框
    private void showNotification(Context context, Bundle bundle) {
        String extras = bundle.getString(JPushInterface.EXTRA_EXTRA);
        reqCode++;
        String notificationType = "0";
        try {
            JSONObject extrasJson = new JSONObject(extras);
            notificationType = extrasJson.optString("Notificaction");
        } catch (Exception e) {
            MyLog.e("Unexpected: extras is not a valid json" + e.toString());
        }
        if (notificationImpl == null) {
            notificationImpl = new NotificationImpl(context);
        }
        notificationImpl.setRequestCode(reqCode);

        switch (notificationType){
            case "0":
                notificationImpl.notify_normal_singLine(bundle);
                break;
            case "1":
                notificationImpl.notify_normal_moreLine(bundle);
                break;
            case "2":
                notificationImpl.notify_mailbox(bundle);
                break;
            case "3":
                notificationImpl.notify_bigPic(bundle);
                break;
            case "4":
                notificationImpl.notify_customview(bundle);
                break;
            case "5":
                notificationImpl.notify_buttom(bundle);
                break;
            case "6":
                notificationImpl.notify_progress(bundle);
                break;
            case "7":
                notificationImpl.notify_headUp(bundle);
                break;
            default:
                break;
        }

    }

    private void receivingNotification(Context context, Bundle bundle) {
        CustomPushNotificationBuilder builder = new
                CustomPushNotificationBuilder(context,
                R.layout.yyb_notification,
                R.id.icon,
                R.id.title,
                R.id.text);
        // 指定定制的 Notification Layout
        builder.statusBarDrawable = R.drawable.down;
        // 指定最顶层状态栏小图标
        builder.layoutIconDrawable = R.drawable.arrow_up;
        // 指定下拉状态栏时显示的通知图标
        JPushInterface.setPushNotificationBuilder(2, builder);

        String title = bundle.getString(JPushInterface.EXTRA_NOTIFICATION_TITLE);
        MyLog.i( " title : " + title);
        String message = bundle.getString(JPushInterface.EXTRA_ALERT);
        MyLog.i( "message : " + message);
        String extras = bundle.getString(JPushInterface.EXTRA_EXTRA);
        MyLog.i( "extras : " + extras);
    }

    private void openNotification(Context context, Bundle bundle) {
        String extras = bundle.getString(JPushInterface.EXTRA_EXTRA);
        String myValue = "";
        try {
            JSONObject extrasJson = new JSONObject(extras);
            myValue = extrasJson.optString("myKey");
        } catch (Exception e) {
            MyLog.e("Unexpected: extras is not a valid json" + e.toString());
            return;
        }

    }

    // 打印所有的 intent extra 数据
    private static String printBundle(Bundle bundle) {
        StringBuilder sb = new StringBuilder();
        for (String key : bundle.keySet()) {
            if (key.equals(JPushInterface.EXTRA_NOTIFICATION_ID)) {
                sb.append("\nkey:" + key + ", value:" + bundle.getInt(key));
            }else if(key.equals(JPushInterface.EXTRA_CONNECTION_CHANGE)){
                sb.append("\nkey:" + key + ", value:" + bundle.getBoolean(key));
            } else if (key.equals(JPushInterface.EXTRA_EXTRA)) {
                if (TextUtils.isEmpty(bundle.getString(JPushInterface.EXTRA_EXTRA))) {
                    MyLog.d("This message has no Extra data");
                    continue;
                }

                try {
                    JSONObject json = new JSONObject(bundle.getString(JPushInterface.EXTRA_EXTRA));
                    Iterator<String> it =  json.keys();

                    while (it.hasNext()) {
                        String myKey = it.next().toString();
                        sb.append("\nkey:" + key + ", value: [" +
                                myKey + " - " +json.optString(myKey) + "]");
                    }
                } catch (JSONException e) {
                    MyLog.e("Get message extra JSON error!");
                }

            } else {
                sb.append("\nkey:" + key + ", value:" + bundle.getString(key));
            }
        }
        return sb.toString();
    }

}

ps:

  • 通知信息和自定义信息的区别是:通知会在通知栏中显示,自定义需要自己出来,可以自定义显示。
  • 通知状态栏也可以自行定义。
  • showNotification可以自行定义通知框的显示。但是需要后台服务器中下发对应的字段进行控制。这里使用了一个开源通知库: https://github.com/wenmingvs/NotifyUtil。 但是小米mui8手机上通知信息达不到效果。在其他的手机上还是有效果。

6.定向推送

在开发中为了定向推送到指定的手机上,所以需要在客户端中绑定相应的别名,这样就可以定向推送到指定的手机上了。因为客户端和服务器端都是自己开发,可以通过用户名做为别名进行绑定。

  // 调用 Handler 来异步设置别名
  if (!user_id.equals(SpUtils.get(getApplicationContext(),"TAG_ALIAS",""))){
     SpUtils.remove(getApplicationContext(),"TAG_ALIAS");
     mHandler.sendMessage(mHandler.obtainMessage(MSG_SET_ALIAS, MUtils.user_id));
  }
  
 private final TagAliasCallback mAliasCallback = new TagAliasCallback() {
        @Override
        public void gotResult(int code, String alias, Set<String> tags) {
            String logs ;
            switch (code) {
                case 0:
                    logs = "Set tag and alias success";
                    MyLog.i(logs);
                    SpUtils.put(getApplicationContext(),"TAG_ALIAS",alias);
                    break;
                case 6002:
                    logs = "Failed to set alias and tags due to timeout. Try again after 60s.";
                    MyLog.i( logs);
                    // 延迟 60 秒来调用 Handler 设置别名
                    mHandler.sendMessageDelayed(mHandler.obtainMessage(MSG_SET_ALIAS, alias), 1000 * 60);
                    break;
                default:
                    logs = "Failed with errorCode = " + code;
                    MyLog.i(logs);
            }
        }
    };

    private static final int MSG_SET_ALIAS = 1001;
    private final Handler mHandler = new Handler() {
        @Override
        public void handleMessage(android.os.Message msg) {
            super.handleMessage(msg);
            switch (msg.what) {
                case MSG_SET_ALIAS:
                    MyLog.i("Set alias in handler.");
                    // 调用 JPush 接口来设置别名。
                    JPushInterface.setAliasAndTags(getApplicationContext(),
                            (String) msg.obj,
                            null,
                            mAliasCallback);
                    break;
                default:
                    MyLog.i( "Unhandled msg - " + msg.what);
            }
        }
    };

这里使用SharedPreferences

public class SpUtils
{
    /**
     * 保存在手机里面的文件名
     */
    public static final String FILE_NAME = "share_data";

    /**
     * 保存数据的方法,我们需要拿到保存数据的具体类型,然后根据类型调用不同的保存方法
     *
     * @param context
     * @param key
     * @param object
     */
    public static void put(Context context, String key, Object object)
    {

        SharedPreferences sp = context.getSharedPreferences(FILE_NAME,
                Context.MODE_PRIVATE);
        SharedPreferences.Editor editor = sp.edit();

        if (object instanceof String)
        {
            editor.putString(key, (String) object);
        } else if (object instanceof Integer)
        {
            editor.putInt(key, (Integer) object);
        } else if (object instanceof Boolean)
        {
            editor.putBoolean(key, (Boolean) object);
        } else if (object instanceof Float)
        {
            editor.putFloat(key, (Float) object);
        } else if (object instanceof Long)
        {
            editor.putLong(key, (Long) object);
        } else
        {
            editor.putString(key, object.toString());
        }

        SharedPreferencesCompat.apply(editor);
    }

    /**
     * 得到保存数据的方法,我们根据默认值得到保存的数据的具体类型,然后调用相对于的方法获取值
     *
     * @param context
     * @param key
     * @param defaultObject
     * @return
     */
    public static Object get(Context context, String key, Object defaultObject)
    {
        SharedPreferences sp = context.getSharedPreferences(FILE_NAME,
                Context.MODE_PRIVATE);

        if (defaultObject instanceof String)
        {
            return sp.getString(key, (String) defaultObject);
        } else if (defaultObject instanceof Integer)
        {
            return sp.getInt(key, (Integer) defaultObject);
        } else if (defaultObject instanceof Boolean)
        {
            return sp.getBoolean(key, (Boolean) defaultObject);
        } else if (defaultObject instanceof Float)
        {
            return sp.getFloat(key, (Float) defaultObject);
        } else if (defaultObject instanceof Long)
        {
            return sp.getLong(key, (Long) defaultObject);
        }

        return null;
    }

    /**
     * 移除某个key值已经对应的值
     * @param context
     * @param key
     */
    public static void remove(Context context, String key)
    {
        SharedPreferences sp = context.getSharedPreferences(FILE_NAME,
                Context.MODE_PRIVATE);
        SharedPreferences.Editor editor = sp.edit();
        editor.remove(key);
        SharedPreferencesCompat.apply(editor);
    }

    /**
     * 清除所有数据
     * @param context
     */
    public static void clear(Context context)
    {
        SharedPreferences sp = context.getSharedPreferences(FILE_NAME,
                Context.MODE_PRIVATE);
        SharedPreferences.Editor editor = sp.edit();
        editor.clear();
        SharedPreferencesCompat.apply(editor);
    }

    /**
     * 查询某个key是否已经存在
     * @param context
     * @param key
     * @return
     */
    public static boolean contains(Context context, String key)
    {
        SharedPreferences sp = context.getSharedPreferences(FILE_NAME,
                Context.MODE_PRIVATE);
        return sp.contains(key);
    }

    /**
     * 返回所有的键值对
     *
     * @param context
     * @return
     */
    public static Map<String, ?> getAll(Context context)
    {
        SharedPreferences sp = context.getSharedPreferences(FILE_NAME,
                Context.MODE_PRIVATE);
        return sp.getAll();
    }

    /**
     * 创建一个解决SharedPreferencesCompat.apply方法的一个兼容类
     *
     * @author zhy
     *
     */
    private static class SharedPreferencesCompat
    {
        private static final Method sApplyMethod = findApplyMethod();

        /**
         * 反射查找apply的方法
         *
         * @return
         */
        @SuppressWarnings({ "unchecked", "rawtypes" })
        private static Method findApplyMethod()
        {
            try
            {
                Class clz = SharedPreferences.Editor.class;
                return clz.getMethod("apply");
            } catch (NoSuchMethodException e)
            {
            }

            return null;
        }

        /**
         * 如果找到则使用apply执行,否则使用commit
         *
         * @param editor
         */
        public static void apply(SharedPreferences.Editor editor)
        {
            try
            {
                if (sApplyMethod != null)
                {
                    sApplyMethod.invoke(editor);
                    return;
                }
            } catch (IllegalArgumentException e)
            {
            } catch (IllegalAccessException e)
            {
            } catch (InvocationTargetException e)
            {
            }
            editor.commit();
        }
    }

}

到此android客户端集成结束。

2.3服务端集成

引入到现有工程中。把1中的jar和测试文件到入到工程中,把3中的jar文件导入到工程中。

jpush_web.png

详细的使用可以参考1中的介绍。我这只对发通知和消息和指定对象推送进行测试

public class Test {
    protected static final Logger LOG = LoggerFactory.getLogger(Test.class);
    public static final String ALERT = "数据";

    public static void main(String[] args) {
        testSendPush();
    }

    public static void testSendPush() {
        JPushClient jpushClient = new JPushClient(Config.masterSecret, Config.appKey);
        PushPayload payload = buildPushObject_all_all_alert();

        try {
            PushResult result = jpushClient.sendPush(payload);
            LOG.info("Got result - " + result);
        } catch (APIConnectionException e) {
            LOG.error("Connection error. Should retry later. ", e);
        } catch (APIRequestException e) {
            LOG.error("Error response from JPush server. Should review and fix it. ", e);
            LOG.info("HTTP Status: " + e.getStatus());
            LOG.info("Error Code: " + e.getErrorCode());
            LOG.info("Error Message: " + e.getErrorMessage());
            LOG.info("Msg ID: " + e.getMsgId());
        }
    }

    public static PushPayload buildPushObject_all_all_alert() {
//        PushPayload payload = PushPayload
//                .newBuilder()
//                .setPlatform(Platform.android())
//                .setAudience(Audience.tag("tag1"))
//                .setNotification(
//                        Notification
//                                .newBuilder()
//                                .setAlert(ALERT)
//                                .addPlatformNotification(
//                                        IosNotification.newBuilder().incrBadge(1)
//                                                .addExtra("extra_key", "extra_value").build())
//                                .build()).build();
//        return payload;
        Map<String, String> extras = new HashMap<String, String>();
        extras.put("Notificaction", "0");
        extras.put("URL", "www.baidu.com");

        Message.content("数据");
        PushPayload payload = PushPayload.newBuilder()
                   .setPlatform(Platform.android())
                   .setAudience(Audience.alias("0123456789"))
                   .setOptions(Options.sendno())
                   .setNotification(Notification.android("一个惊喜", "号外", extras))
                   //.setMessage(Message.newBuilder().setMsgContent("").addExtra("Notificaction", "0").build())
//                   .setMessage(
//                        Message.newBuilder().setMsgContent("数据").addExtra("Notificaction", "7").setTitle("大爷的")
//                                .build())
                   .build();
                   
                   
        return payload;
        
    }

}

到处服务端集成成功。

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

推荐阅读更多精彩内容