数据存储入门指南

AVOS Cloud 移动开发 SDK 为 iOS、Android 和 WindowsPhone® 应用程序提供了基于云的 API 和服务,并且还提供了 JavaScript 和 REST API。使用 AVOS Cloud API,您可以极快地以最少工作量让您的移动应用程序支持云处理。集成了 AVOS Cloud SDK 的移动应用程序可以轻松地在 AVOS Cloud 云上存储数据对象和文件,发送并侦听推送通知,管理用户,处理地理位置数据,并可以轻松获得移动 IM 的能力。

开始之前
出于本文的目的,我假设您已经非常熟悉使用 JSON、Android 和 Eclipse 进行移动应用编程的基本概念。在您继续阅读本文之前,请访问 avoscloud.com 并创建您的应用程序。只需遵循注册页面中的简单指令即可。

本文介绍了包含用户、数据对象和文件的核心 API 类。您将学习如何使用访问控制列表 (ACL),以及如何在数据对象上执行 CRUD 操作,还有如何在 AVOS Cloud 云端存储和检索文件。示例均构建于 AVOS Cloud SDK for Android 之上(请参阅Android开发指南)。

AVOS Cloud 控制台

AVOS Cloud 控制台可以帮助开发人员管理应用程序。该控制台为 API、文件和推送的通知提供了一般指标和应用程序特定的使用指标。通过控制台可管理应用程序的基本设置信息,它还提供了数据浏览器,使开发人员能够浏览(甚至编辑)所存储的数据对象。数据浏览器对于调试非常有用。图 1 是 AVOS Cloud 控制台的屏幕截图:

AVOS Cloud 控制台
AVOS Cloud 控制台

通过一个 App ID 和 App Key 对应用程序进行身份验证。为了获得您的应用程序 ID 和 Key,必须通过 AVOS Cloud 控制台注册您的应用程序。在您的应用程序上初始化 AVOS Cloud SDK 时,会用到这些信息。

AVObject 数据对象

在 AVOS Cloud 中,使用 “键-值” 对的容器 AVObject 表示数据。AVObject 可以存储任何与 JSON 兼容的数据,如清单 1 所示:

AVObject myObject = new AVObject("MyObject"); // Class Namemy
Object.put("name", “Feng Junwen");
myObject.put("twitterHandle", “jwfing");
myObject.put("followers", 123456);

AVObject 在实例化时被赋予一个 classname(类名称)。在上述代码中,类名称是 "MyObject"。类名称与关系数据库中的表名称相似,同一类的 AVObject 对象相当于表中的行。

AVObject 暴露的方法类似于 Java Map 类中的方法,如 put、get 和 remove,以及大量特定于 AVObject 的其他方法。

AVObject名称键 必须是字母数字的,作为一个指导方针,请对名称键使用驼峰式大小写 (camel-casing) 格式。值可以是存储在 JSON 中的任何数据类型,也就是说,可以是数字、字符串、布尔值、数组、JSONObject.NULL、JSONObject 和 JSONArray。AVObject 所支持的其他数据类型是 Java Date 和 byte[] 数组。AVObject 还可以嵌套包含其他 AVObject。

清单 2 显示了部分受支持的 AVObject 值数据类型:

// Byte Array
byte[] byteArray = {1, 2, 3, 4, 5};
// A date
Date d = new Date(); // java.util.Date
// A number
int number = 21;
// A String
String name = "Enrique";
// A JSONArray - any mix of JSONObjects, JSONArrays, Strings, Booleans, 
//   Integers, Longs, Doubles, null or NULL.
JSONArray jArray = new JSONArray();
jArray.put(number);
jArray.put(name);
// A JSONObject 
JSONObject jObject = new JSONObject();
try {
    jObject.put("number", number);
    jObject.put("name", name);
} catch (JSONException e) {
    e.printStackTrace();
}
// A AVObject
AVObject pObject = new AVObject("MyObject"); // Class name
pObject.put("myByteArray", byteArray);
pObject.put("myDate", d);
pObject.put("myString", name);
pObject.put("myNumber", number);
pObject.put("myJsonArray", jArray);
pObject.put("myJsonObject", jObject);
pObject.put("myNull", JSONObject.NULL);

上述代码创建了一个 AVObject,它在 AVOS Cloud 云端被存储为一个对象。之后,我们可以保存、查询和更新,并能从云端存储中删除。甚至可以在应用程序离线时保存数据,AVOS Cloud SDK 将数据保存在本地,直到重新建立网络连接。

修改 AVObject

如果您熟悉移动应用程序开发,那么您就会知道,网络操作等长时间操作一般都是在后台完成,或在一个工作线程上完成,而不是在主系统的 UI 线程上完成。这样可以避免主线程发生阻塞并影响用户界面的响应速度。稍后,在本文的后半部分,我会告诉您 AVOS Cloud 如何为后台工作提供便利,从而保存、删除和查找对象。现在,考虑下面的同步 remove() 方法,可以使用它从 AVObject 删除一个字段:

pObject.remove("myNumber"); // remove the field/key "myNumber" from pObject

在删除或添加字段后,或在更新当前字段后,您可以在云端保存(或更新)数据对象,只需调用其中一个 AVObject 的 save...() 方法,我稍后会在本文中讨论它们。

AVOS Cloud 用户、角色和 ACL

在我告诉您如何在 AVOS Cloud 对象上执行 CRUD 操作之前,您应该了解一下 用户、角色和 ACL(访问控制列表)。这三个都是非常重要的概念,有助于保护您的应用程序的数据对象。

用户

名称为 AVUser 的类代表一个用户,并为应用程序提供帐户管理功能。每个应用程序都有与之关联的 AVUser 表。一个 AVUser 也是一个 AVObject,但具有更多的属性,如用户名、密码和电子邮件。您可以添加任何您认为合适的其他数据值。

AVOS Cloud 中的匿名用户
在 AVOS Cloud 中,匿名用户是没有用户名或密码的用户。匿名用户对于那些不要求用户身份验证的移动应用程序功能非常有用。匿名用户可以创建数据对象,但这些对象寿命很短,并且在匿名用户登出之后就不再可用。

用户可以注册成为您应用的 AVUser 用户,如清单 3 (AVUser — 注册)所示:

AVUser user = new AVUser();
user.setUsername(“Feng Junwen");
user.setPassword(“123456&asdQWE");
user.setEmail(“jfeng@avoscloud.com");
user.put("userType", "Author"); // add another field
// Call the asynchronous background method to sign up 
user.signUpInBackground(new SignUpCallback() {
  public void done(AVException e) {
    if (e == null) {
      // Successful. Allow access to app.
    } else {
      // Failed....
    }
  }
});

username 和 email 必须是应用内惟一的。如果 username 或 email 已经被使用,那么注册调用将会失败。您应该有一个通知用户字段限制的机制,并提供一个重试的进程。

在注册之后,用户就可以登录您的应用,如清单 4 (AVUser — 登录)所示:

AVUser.logInInBackground(“Feng Junwen", “123456&asdQWE", new LogInCallback() {
  public void done(AVUser user, AVException e) {
    if (user != null) {
      // Successful. Allow access to app.
    } else {
      // Failed
    }
  }
});

您可以通过调用 AVUser.save() 更新用户信息。 但是请注意,只有 AVUser 的所有者可以修改其内容,数据对于其他任何人都是只读的。

AVOS Cloud SDK 会缓存当前登录的用户。您可以通过调用 AVUser.currentUser() 查询当前用户。currentUser 方法使您能够快速访问当前用户信息,那么如果当前用户会话没有激活,您只需要根据提示输入验证。清单 5 (AVUser — 获得当前用户)显示了如何获取当前用户:

AVUser currentUser = AVUser.getCurrentUser();
if (currentUser != null) {
  // current user is valid
} else {
  // current user not valid, ask for credentials
}

重置当前用户

在 AVOS Cloud 中,您可以通过调用 AVUser.logOut() 来重置当前用户,如清单 6(AVUser — 重置当前用户) 所示:

AVUser.logOut(); // static method

ACL

ACL 是关联到数据对象的访问权限(或控制)列表。AVACL 类允许您为给定的 AVObject 定义权限。使用 ACL,您可以定义对您的应用程序数据对象的公共访问,并且可以(通过角色)限制对特定用户或用户组的访问。清单 7 演示了 AVOS Cloud ACL 的用法:

// Define a AVOS Cloud user
AVUser user = new AVUser();
user.setUsername(username);
:
:
// Define a read/write ACL
AVACL rwACL = new AVACL();
rwACL.setReadAccess(user, true); // allow user to do reads
rwACL.setWriteAccess(user, true); // allow user to do writes
:
:
// Define a AVOS Cloud object and its ACL
AVObject gameObject = new AVObject("Game");
gameObject.setACL(rwACL); // allow user do read/writes on gameObject
gameObject.saveInBackground(); // save the ACL'ed object to the cloud
:
:
// You can define a public ACL that gives public access to the object
AVACL publicACL = new AVACL();
publicACL.setPublicReadAccess(true);
publicACL.setPublicWriteAccess(true);      
gameObject.setACL(publicACL); // allow public read/writes
gameObject.saveInBackground(); // save the ACL'ed object to the cloud

您还可以为所有新创建的对象定义一个默认的 ACL。在清单 8 中,我已经将可读取和写入的默认 ACL 设置为 public,清单 7 中的 publicACL定义。

清单 8. 设置默认的 ACL

// Set a default ACL for all newly created objects as public access
AVACL.setDefaultACL(publicACL, true);

虽然在这里没有演示,但我们也可以使用 AVRole 类为用户组授予访问权限。接下来,我们将查看如何将 AVObject 数据对象保存到 AVOS Cloud 云,并在云端检索它们。

保存 AVObject 数据对象

一旦创建并填充了 AVObject,就可以在 AVOS Cloud 云上保存。在云端保存数据对象实际上是利用 AVOS Cloud 最简单的操作之一,其复杂性通常与数据表示、编组、网络通信和传输等有关联,AVOS Cloud SDK 完全隐藏了此复杂性。您需要使用 helper 方法来将数据对象实例映射到 AVObject,并映射回来,您需要决定是在您自己的线程上调度 AVOS Cloud 保存操作,还是使用 save-in-the-background 方法。

谨防系统线程阻塞!
回想一下,在移动应用程序中,长时间的操作(如网络、文件或长的计算)不应该在主系统线程上完成。相反,应在一个单独的工作线程中执行它们。阻塞系统线程会对应用程序的用户界面的响应能力产生负面影响,有可能导致强行关闭您的应用程序。

AVObject 提供两个保存方法: save() 和 saveInBackground()。 saveInBackground()是建议的保存方法,因为它在自己的工作线程上运行保存操作。如果您选择使用同步的save() 方法,需要注意的是,您需要在该方法自己的工作线程上调用该方法,以避免 UI 被阻塞。

清单 9 (在后台保存 AVObject)显示的代码在后台保存 AVObject 数据对象:

AVObject pObject = new AVObject("ExampleObject");
pObject.put("myNumber", number);
pObject.put("myString", name);
pObject.saveInBackground(); // asynchronous, no callback

清单 10(使用回调,在后台保存) 显示的代码使用了一个回调,在后台保存 AVObject 数据对象:

pObject.saveInBackground(new SaveCallback () {
    @Override
    public void done(AVException ex) {
        if (ex == null) {
            isSaved = true;
        } else {
            // Failed
            isSaved = false;
        }
    }
  });

save...() 方法的变形包括以下几种:

  • 使用或不使用回调,saveAllinBackground() 保存 AVObject。
  • saveAll(List<AVObject> objects) 保存 AVObjects 的列表。
  • saveAllinBackground(List<AVObject> objects) 在后台保存 AVObjects 的列表。
  • saveEventually() 让您能够在未来某个时点将数据对象保存到服务器;如果 AVOS Cloud 云目前不可访问,则使用该方法。

一旦在云上已成功保存了 AVObject ,就会为对象提供惟一的 Object-ID。此 Object-ID 非常重要,因为它惟一地标识了此 AVObject 实例。例如,您可以使用 Object-ID 确定是否已在云上成功保存该对象,以便检索或刷新给定的对象实例和删除特定的 AVObject。

检索数据对象

这一节将探讨查询和检索 AVOS Cloud 云上的数据对象的方法。您可以通过 object-ID 查询一个 AVObject,您也可以使用属性查询一个或多个 AVObject 对象。如果您已经有一个 AVObject,那么您可以通过从服务器提取最新的值,刷新或同步其内容。我们将在以下代码片段中探讨所有这些选项。

提取 AVObjects

为了从 AVOS Cloud 云提取数据对象,使用 AVObject 方法 fetch() 或 fetchInBackground(),如清单 11(提取) 所示:

AVObject pObject = new AVObject("ExampleObject");
:
:
// Fetch the AVOS Cloud object unconditionally
try {
    pObject.fetch();
} catch (AVException e) {
    e.printStackTrace();
}
// Fetch the AVOS Cloud object unconditionally, with Callback
pObject.fetchInBackground(new GetCallback() {
    @Override
    public void done(AVObject obj, AVException ex) {
        if (ex == null) {
            // Success
        } else {
            // Failed
        }
    }
});

您也可以仅在需要时提取数据,例如,提取一个 AVOS Cloud 对象关联的 AVObject 对象,如清单 12(根据需要进行提取):

AVObject po = new AVObject("ExampleObject");
:
AVObject po2 = po.getAVObject("key");
// Fetch only if needed
try {
    po2.fetchIfNeeded();
} catch (AVException e) {
    e.printStackTrace();
}

另一个选项是在后台根据需要执行提取操作,并使用一个回调。为此,可以使用 AVObject 的 fetchIfNeededInBackground(GetCallback callback) 方法。

在某些情况下,您需要无条件地或仅在需要时一次提取一个 AVObject 集合。AVObject 为此提供了一组静态方法,每个方法都将一个 AVObject 的列表作为输入,然后返回一个 AVObject 的列表:

  • fetchAll(List<AVObject> objects)
  • fetchAllIfNeeded(List<AVObject> objects)
  • fetchAllIfNeededInBackground(List<AVObject> objects, FindCallback callback)
  • fetchAllInBackground(List<AVObject> objects, FindCallback callback)

查询数据对象

希望您现在已看到 AVOS Cloud API 的强大,但是别着急,它还有更多功能!除了提取数据对象之外,AVOS Cloud 还可以让您使用 object-ID 或属性来查询数据对象。要从云端查询一个数据对象,可以使用 AVQuery。您可以使用 AVQuery 进行基本的和复杂的数据查询,接收给定的对象或匹配对象的 List。

清单 13(使用 AVQuery 检索给定的 AVObject) 显示了如何在一个后台线程中使用给定 object-ID 从服务器检索特定的 AVObject:

String myID = "12345";
AVQuery query = new AVQuery("Players");
query.getInBackground(myID, new GetCallback() {
    @Override
    public void done(AVObject object, AVException e) {
        if (object != null) {
            // Get object
        } else {
            // Not found
        }
    }
});

请注意,query.getInBackground() 没有使用 AVQuery 缓存。

清单 14(对给定类的所有 AVObjects 使用 AVQuery) 显示的查询检索 Players 类的所有数据对象。(没有提供约束。)

AVQuery query = new AVQuery("Players");
query.findInBackground(new FindCallback() {
    @Override
    public void done(List<AVObject> players, AVException e) {
        if (players != null) {
            // Get list of players
        } else {
            // No players
        }
    }
});

在清单 15(使用 AVQuery 检索匹配的 AVObjects) 中,我使用了一个查询约束来检索一个或多个匹配的 AVObject;在本例中,希望找到所有活跃的 Players:

AVQuery query = new AVQuery("Players");
query.whereEqualTo("status", "active");
query.findInBackground(new FindCallback() {
    @Override
    public void done(List<AVObject> players, AVException e) {
        if (players != null) {
            // Success - players contain active players
         } else {
            // Failed
        }
    }
});

AVQuery 还提供了一个方法来获得匹配对象的计数,不需要检索对象本身,这非常有用。清单 16(使用 AVQuery 对匹配的 AVObjects 进行计数) 展示了如何获得活跃玩家的计数:

AVQuery query = new AVQuery("Players");
query.whereEqualTo("status", "active"); //remove this line to count ALL
query.countInBackground(new CountCallback() {
    @Override
    public void done(int count, AVException e) {
        if (e == null) {
            // Success, see count variable
        } else {
            // Failed
        }
    }
});

AVQuery 方法和约束

AVQuery 支持 20 多个不同的查询约束方法,以下是一些示例:

  • whereMatches(String key, String regex) 查找与所提供的正则表达式相匹配的字符串值。
  • whereStartsWith(String key, String prefix) 查找使用所提供的字符串开头的字符串值。
  • whereContains(String key, String substring) 查找包含所提供的字符串的值。
  • whereGreaterThan(String key, Object value) 查找大于所提供的值的值。
  • whereWithinKilometers(String key, AVGeoPoint point, double maxDistance) 查找点值在给定点附近,并且在给定最大距离内的对象。

查询结果可以被排序,如清单 17(排序查询结果) 所示。为了对查询结果进行排序,可以调用其中一个 query orderBy...() 方法,指定作为排序依据的字段。

query.orderByAscending("name");
query.orderByDescending("name");
// For example:
AVQuery query = new AVQuery("Players");
query.whereEqualTo("status", "active");
query.orderByAscending("lastName"); // By lastname ascending order
query.findInBackground(new FindCallback() {
    @Override
    public void done(List<AVObject> players, AVException e) {
    if (players != null) {
      // Success - players contain active players
    } else {
      // Failed
    }
   }
});

另外,需要注意的是,AVQuery 结果被缓存。您可以根据应用程序的需求,采用不同的方式设置查询缓存策略。目前支持以下缓存策略:

  • IGNORE_CACHE:不使用缓存,这是默认的缓存策略。
  • CACHE_ONLY:只从缓存加载。如果没有已缓存的结果,将会有一个 AVException。
  • NETWORK_ONLY:始终从网络加载,但将结果保存到缓存。
  • CACHE_ELSE_NETWORK:首先查找缓存。如果失败,则从网络加载。如果缓存与网络均不成功,结果将是 AVException。
  • NETWORK_ELSE_CACHE:首先查找网络。如果失败,则从缓存加载。如果缓存与网络均不成功,结果将是 AVException。
  • CACHE_THEN_NETWORK:首先查找缓存。如果失败,则从网络加载。请注意,FindCallback 实际上被调用了两次:第一次包括缓存的结果,然后包括网络结果。该策略只能与 findInBackground 异步使用。

设置缓存策略很简单。设置之前,首先要调用 find...() 方法,如清单 18 所示:

清单 18. 管理 CachePolicy

AVQuery query = new AVQuery("Players");
query.setCachePolicy(AVQuery.CachePolicy.NETWORK_ELSE_CACHE);
query.findInBackground(new FindCallback() {
    @Override
    public void done(List<AVObject> players, AVException e) {
        if (e == null) {
            // Success - players contain active players
         } else {
            // Failed
        }
    }
});

删除数据对象

从 AVOS Cloud 云端删除数据对象也很简单。如果您已经有了对象实例,那么您就可以通过调用 AVObject 的 delete() 或 deleteInBackground()方法,从云中删除现有的 AVObject。这两个方法如清单 19 所示:

清单 19. 从云中删除 AVObject

pObject.delete();
pObject.deleteInBackground();

如您所料,delete() 是一个阻塞调用,这意味着您需要负责适当地在自己的工作线程上调用它。或者可以使用带有或不带有回调的deleteInBackground() 方法来完成这一操作。

使用文件

至此,我们一直在使用 AVObject 对象和 AVQuery 查询。我还向您介绍了 AVUser(用户)、ACL 和角色。最后,我将示范如何在 AVOS Cloud 中使用文件的读、写和保存函数。

回想一下,您可以在 AVObject 中存储原始 byte[] 数据,这对于小规模数剧来说是没问题的。但在存储较大的项目(如图像或文档)时,应该使用 AVFiles。

AVOS Cloud 云端的文件使用 AVFile 来表示,它提供获得文件名、URL、文件数据(假设数据可用)的方法。您也可以下载和上传文件,并访问其他一些辅助方法。

文件数据以 byte[] 语法表示。请注意,目前,给定的文件不得超过 10MB。在命名文件时,需要避免潜在的名称冲突,提供文件扩展名可以帮助 AVOS Cloud 处理文件的内容。

清单 20(保存一个 AVFile) 演示了保存一个 JPG 文件:

Drawable drawable = ...;
Bitmap bitmap = (Bitmap)((BitmapDrawable) drawable).getBitmap();
ByteArrayOutputStream stream = new ByteArrayOutputStream();
bitmap.compress(Bitmap.CompressFormat.JPEG, 100, stream);
byte[] data = stream.toByteArray();
AVFile imageFile = new AVFile("image.jpg", data);
imageFile.saveInBackground();

上述代码中最初的语句将位图转换成一个byte[]。然后,使用 AVFile.saveInBackground() 方法保存 byte[],这与将 AVObject 保存在服务器上的方法类似。

一旦将文件保存到 AVOS Cloud 上,必须将它与 AVOject 关联(放入 AVOject)。换句话说,AVFile 文件不是真正的独立对象,并且为了以后检索和使用,必须将它关联到给定的 AVObject 实例。这种局限可能会在未来版本中进行处理。

清单 21 将图像文件关联到一个 Player 对象:

AVObject po = new AVObject("Players");
po.put("name", "eortiz");
po.put("photo", imageFile);
po.saveInBackground();

我已将文件关联到数据对象,然后使用 saveInBackgroud() 方法将对象保存在服务器上,我在前面已讨论过这一点。

清单 22 显示了如何检索与数据对象有关联的文件:

AVFile imageFile2 = (AVFile)po.get("photo");
imageFile2.getDataInBackground(new GetDataCallback() {
  public void done(byte[] data, AVException e) {
    if (data != null) {
      // Success; data has the file
    } else {
      // Failed
    }
  }
});

从 AVObject 对象收到 AVFile 引用之后,我调用 getDataInBackground() 从服务器中检索 AVFile。请注意,我使用了回调GetDataCallback 来检索 AVOS Cloud 文件,并没有使用 GetCallback,后者用于通过 AVQuery 检索 AVObject 对象。

结束语

AVOS Cloud API 非常全面,包括访问移动服务的类,比如实时消息,推送通知,使用地理数据,移动统计等等。在本文中,我通过介绍 AVOS Cloud API 实现数据和文件的云存储,简单介绍了如何使用 AVOS Cloud。我们还了解了如何在 AVOS Cloud 云端存储和操纵用户、数据对象、文件和 ACL,这是进一步探索云平台进行移动开发的良好基础。

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

推荐阅读更多精彩内容