-
Notifications
You must be signed in to change notification settings - Fork 28
1.存储 7 Status
信息流 API 仅在 6.1.0 (含)之后版本可用。
LeanCloud 存储服务提供了信息流 API,以便于开发者在自己的产品中加入「时间线」、「朋友圈」或「私信」等类似的功能。
首先明确几个基本概念:
- 用户关系,指社交应用中用户关注/加粉等行为构建成的群体关系,例如微博里面的「关注者」和微信中的「好友」关系,本文会统一按照关注关系来描述。假设一个用户 A 有 ta 关注的用户群体,也有关注 ta 的用户群体存在。
- 状态,指用户发出来的即时状态,例如微博中的一个帖子,微信朋友圈中的一段文字,就是这里所说的「状态」,在 API 中用
Status
表示。 - 时间线。在社交类产品中,用户 A 关注了一些人之后,他就可以按照时间顺序查看到那些人发布的各种「状态」信息(这里我们暂且把这些信息称为「时间线」);同样,A 发布了某一条「状态」之后,一般就会马上被关注 ta 的其他人看到,但是也有例外(就是接下来的「私信」)。
- 私信,是状态中的一类特殊信息,只发给特定的用户,不会进入关注者的时间线来公开展示,例如微博里面的「私信」即是如此。
我们的信息流 API 主要提供以下功能:
- 用户 A 给用户 B 发送私信(发私信);
- 用户 A 给关注 ta 的用户群体发送公开的状态信息(发推和转推,等);
- 用户 B 可以查看别人给他发送的私信(私信列表);
- 用户 B 可以查看 ta 关注的用户群体发出来的状态信息(查看时间线);
- 用户 C 可以公开查看用户 A 发出去的所有公开的状态信息(查看单个用户的公开状态);
- 用户 A 可以删除自己已经发布的状态,此时所有接收者的收件箱也会自动删除该状态数据;
- 用户 A 可以删除/屏蔽自己时间线上其他人发出来的状态(只在自己时间线上不显示,发布者和其他关注者依然可以看到);
以下的功能我们无法在基础 API 层面提供支持:
- 用户 B 根据用户关系中的分组设置来查看不同的人发出来的状态信息;
- 状态发出之后无法修改(可以通过删除 + 发布来实现);
同时,信息流 API 是与用户账号系统绑定以及好友关系 API 绑定的,如果不使用这些内置组件,将无法正常使用信息流功能。接下来我们通过一些例子,看看如何使用信息流 API 来完成产品开发。
假设我们要开发一个类似微博的产品,其中有用户 A 已经关注了 2 个用户(B 和 C),同时他有 3 个关注者(D、E、F)。
第一步,我们来看看用户 A 如何发布一条公开的「状态」。
开发者在 Status 中可以加入任意的数据,由应用层来对这些数据进行解析。信息流 API 提供了多种方式来构造一个 Status 实例:
class AVStatus {
// 完全空的 Status
AVStatus()
// 默认的支持图文混合的 Status
AVStatus(String imageUrl, String message)
// 附带其他自定义属性的 Status
AVStatus(Map<String, Object> customData)
// 设置与获取自定义属性
void put(String key, Object value)
Object get(String key)
// 删除某个自定义属性
void remove(String key)
}
使用方法在类的声明中应该可以一目了然。假如用户 A 想分享一下他中午吃的美食,那么可以按照如下方法来构造一条状态信息:
AVStatus status = new AVStatus("https://ww4.sinaimg.cn/bmiddle/691c275bgy1g9cdxpw3f4j21hc1hc4qq.jpg", "北京烤鸭,贼拉好吃");
status.put("location", "铁岭市赵大胡同");
这一条 Status 如果以公开的方式发送出去,所有关注 A 的用户都会看到这条信息。如果 A 只想发送给用户 D ,他可以选择私信的方式来发送,那就只有用户 D 可以查看到了(这两种方式下一节会说明)。
AVStatus 类中有如下属性被系统保留了:
- inboxType,String,表示发布时指定的访问类型,目前有
private
和default
两种类型。-
private
表示是私信,不会公开显示; -
default
表示是公开信息,会进入到关注者的时间线; - 默认值为
default
; - 开发者可以在应用层面扩展这个 inboxType。例如,微博上就有「系统通知」这一类消息,我们可以增加一种「notification」的 inboxType,来实现这一类消息的收发展示。
-
- source,Pointer,指向状态发布者(AVUser)的指针
- messageId,Integer,LeanCloud 服务端在将状态插入目标用户的接收队列时,生成的一个序列号,主要用于状态流的分页遍历(这在后面会说明)。
- updatedAt, createdAt, objectId,这些 AVObject 通用属性。
这些属性都可以通过 getter/setter 方法进行访问。
我们提供了三种方式来发送 Status:
class Status {
public Observable<AVStatus> sendToFollowersInBackground();
public Observable<AVStatus> sendToFollowersInBackground(String inboxType);
public Observable<AVStatus> sendToUsersInBackground(AVQuery query);
public Observable<AVStatus> sendToUsersInBackground(String inboxType, AVQuery query);
public Observable<AVStatus> sendPrivatelyInBackground(final String receiverObjectId);
}
分别对应不同的使用场景:
- 直接发布给用户自己的关注者(
sendToFollowersInBackground()
)。
这是最常见的方式,用户 A 完成 Status 的构造之后,直接就公开发布出去。这条 Status 就会自动进入关注 ta 的用户的时间线。
另外,考虑到应用层对 inboxType 扩展的需要,我们还提供了一个指定 inboxType 的公开发布 Status 的方法:sendToFollowersInBackground(String inboxType)
,供开发者按需选择使用。
- 选择性的发布给某些用户(
sendToUsersInBackground(AVQuery query)
)。
用户 A 可能会有这样的需求,希望自己发布的 Status 只被一部分好友看到,这时候就可以在发布 Status 的时候,指定一个 AVQuery 实例来圈定目标用户。这是一种比较高阶的做法,具体细节后面会进行详细说明。
- 以私信的形式单独发送给某个用户(
sendPrivatelyInBackground(final String receiverObjectId)
)。
这也是比较常见的需求,某条 Status 只定向发送给某个好友,这时候默认就转变成了「私信」。
我们还是回头来看用户 A 发送美食帖子的实现方法。假设 A 就是公开发布给所有的关注者,那么只需要一次调用就可以完成目的:
status.sendToFollowersInBackground()
.subscribe(new Observer<AVStatus>() {
@Override
public void onSubscribe(Disposable disposable) {
}
@Override
public void onNext(AVStatus avNull) {
// succeed
}
@Override
public void onError(Throwable throwable) {
// failed
throwable.printStackTrace();
}
@Override
public void onComplete() {
}
});
当然,这里有一个前提是用户 A 已经完成了登录,就是 AVUser.currentUser
必须存在且是登录状态,否则发送会失败。
用户 A 发布了状态之后,关注 ta 的用户 D 该如何查看这些信息呢?我们提供了多种方法,可以来查询和展示 Status。
假设用户 D 要查看自己的时间线,他首先需要登录,然后可以调用如下方法来查询自己的时间线状态:
AVUser userD = AVUser.currentUser();
String defaultInbox = AVStatus.INBOX_TYPE.TIMELINE.toString(); // "default"
AVStatusQuery statusQuery = AVStatus.inboxQuery(userD, defaultInbox);
statusQuery.findInBackground().subscribe(new Observer<List<AVStatus>>() {
@Override
public void onSubscribe(Disposable disposable) {
}
@Override
public void onNext(List<AVStatus> avStatuses) {
// succeed
// 这时候每一个 AVStatus 实例都会有 source,messageId,objectId,createdAt,inboxType 等预留属性,也会有开发者自定义设置的所有属性值。
}
@Override
public void onError(Throwable throwable) {
// failed.
throwable.printStackTrace();
}
@Override
public void onComplete() {
}
});
用户 D 要查看自己的私信列表,其方法与查看时间线类似,只需要把 inboxType 变为 AVStatus.INBOX_TYPE.PRIVATE
即可,示例代码如下:
AVUser userD = AVUser.currentUser();
String inboxType = AVStatus.INBOX_TYPE.PRIVATE.toString(); // "private"
AVStatusQuery statusQuery = AVStatus.inboxQuery(userD, inboxType);
statusQuery.findInBackground().blockingLast();
查询时间线和私信都需要当前用户是登录状态,且只能查询当前登录用户能接收到状态流。有时候我们产品里还需要展示单个用户发布的所有公开状态,例如微博的个人主页上会展示 ta 发布的所有帖子。
要查看目标用户的公开 Status,并不要求该用户当前是登录状态,我们只需要知道目标用户的 objectId(AVUser 的基本属性之一) 就可以查看 ta 的公开状态了。
示例代码如下:
AVUser targetUser = AVObject.createWithoutData(AVUser.class, targetUserObjectId);
AVStatus.statusQuery(targetUser)
.findInBackground()
.subscribe(new Observer<List<AVStatus>>() {
@Override
public void onSubscribe(Disposable disposable) {
}
@Override
public void onNext(List<AVStatus> avStatuses) {
// succeed
// 这时候每一个 AVStatus 实例都会有 source,messageId,objectId,createdAt,inboxType
// 等预留属性,也会有开发者自定义设置的所有属性值。
}
@Override
public void onError(Throwable throwable) {
// failed
throwable.printStackTrace();
}
@Override
public void onComplete() {
}
});
从上面的示例代码可以看到,我们可以通过 StatusQuery 实例来完成多种 Status 数据查询,查询结果里所有 Status 实例按照发布时间从新到旧的顺序进行排列。不过如果 Status 数据量较大,无法一次查询(默认 100 条)获取全部数据,这时候就需要进行分页查询,我们该如何做呢?
StatusQuery 还提供了分页获取结果的方法:nextInBackground
,它可以在当前查询的基础上,继续往后获取结果数据。例如,用户 D 使用前述的接口获取了头 100 条结果之后,要继续拉取时间线数据,可以这样做:
// AVStatusQuery statusQuery = AVStatus.inboxQuery(userD, defaultInbox);
statusQuery.nextInBackground().subscribe(new Observer<List<AVStatus>>() {
@Override
public void onSubscribe(Disposable disposable) {
}
@Override
public void onNext(List<AVStatus> avStatuses) {
// succeed
// 这时候每一个 AVStatus 实例都会有 source,messageId,objectId,createdAt,inboxType 等预留属性,也会有开发者自定义设置的所有属性值。
}
@Override
public void onError(Throwable throwable) {
// failed.
throwable.printStackTrace();
}
@Override
public void onComplete() {
}
});
nextInBackground()
可以一直调用,直到没有新的结果返回为止。
StatusQuery 默认都是按照从新到旧的顺序来查询结果,并且每次查询默认的结果集大小为 100。实际上,StatusQuery 还允许开发者来设置多种查询参数,如:
- 查询方向。可以通过
setDirection(PaginationDirection direct)
方法来设置是从新往旧查询(最近发布的 Status 会最先返回),还是从旧到新查询 Status 数据(最早发布的 Status 会最先返回)。 - 分页大小。可以通过
setPageSize(int pageSize)
方法来设置分页的大小,允许的页面大小区间为 (0,1000)(不包含两个端点)。 - 查询的起点和终点。在对时间线进行分页查询的时候,还允许开发者设置查询起点和终点 Status 的 MessageId(查询结果中不会包含两个端点的数据)。
-
setSinceId(long sinceId)
用来设置结果集中最小的 messageId,返回的所有 Status 的 messageId 都会比 sinceId 大。 -
setMaxId(long maxId)
用来设置结果集中最大的 messageId,返回的所有 Status 的 messageId 都会比 maxId 小。
-
如果用户 D 需要按照从旧到新的顺序浏览时间线 Status,并且每次只希望看 20 条数据,那么可以这样实现:
AVUser userD = AVUser.currentUser();
String defaultInbox = AVStatus.INBOX_TYPE.TIMELINE.toString(); // "default"
AVStatusQuery statusQuery = AVStatus.inboxQuery(userD, defaultInbox);
statusQuery.setPageSize(20);
statusQuery.setDirection(PaginationDirection.OLD_TO_NEW);
List<AVStatus> first20Statuses = statusQuery.findInBackground().blockingLast();
List<AVStatus> second20Statuses = statusQuery.nextInBackground().blockingLast();
与 Query 类似,StatusQuery 提供以下方法:
Observable<Integer> countInBackground()
来查询符合条件的 Status 数量,这可以用来查询单个用户的公开 Status 总数。
对于时间线的计数,就不能使用上面的方法了。对于时间线中 Status 的计数信息,因为后端的查询方式不一样,同时也由于产品层对查询结果的要求不一样(需要返回总数和未读数两个维度的统计信息),StatusQuery 提供了新的方法来查询计数信息:
Observable<JSONObject> unreadCountInBackground()
该函数的返回值是一个 JSONObject,包含 total
和 unread
两个计数值。
例如,用户 D 要查看自己收到的私信的计数信息,可以按照如下办法来查询:
AVUser userD = AVUser.currentUser();
AVStatus.inboxQuery(userD, AVStatus.INBOX_TYPE.PRIVATE.toString())
.unreadCountInBackground()
.subscribe(new Observer<JSONObject>() {
@Override
public void onSubscribe(Disposable disposable) {
}
@Override
public void onNext(JSONObject jsonObject) {
// succeed.
}
@Override
public void onError(Throwable throwable) {
// failed.
}
@Override
public void onComplete() {
}
});
要展示用户 A 发布的状态总数,可以按照如下办法来查询:
AVUser targetUser = AVObject.createWithoutData(AVUser.class, userAObjectId);
AVStatus.statusQuery(targetUser)
.countInBackground()
.subscribe(new Observer<Integer>() {
@Override
public void onSubscribe(Disposable disposable) {
}
@Override
public void onNext(Integer integer) {
}
@Override
public void onError(Throwable throwable) {
}
@Override
public void onComplete() {
}
});
我们提供删除 Status 的接口,但是只在以下条件满足的时候调用才会成功:
- 当前用户必须是登录状态。
- 当前登录用户可以删除自己发布的 Status,此时 Status 会从源头上被删除,当前用户的关注者时间线上就不会再出现这条 Status。
- 当前登录用户可以删除自己时间线上他人发布的 Status,此时 Status 不会从源头上被删除,其他关注者在自己的时间线上依然可以看到这条 Status。
Status 的删除接口如下:
Observable<AVNull> deleteInBackground();
假设用户 A 发出那条美食的帖子之后,觉得图片不够精美,想删除它,那么在客户端可以这样执行删除:
// AVStatus target = ...;
target.deleteInBackground()
.subscribe(new Observer<AVNull>() {
@Override
public void onSubscribe(Disposable disposable) {
}
@Override
public void onNext(AVNull v) {
// succeed
}
@Override
public void onError(Throwable throwable) {
// failed
}
@Override
public void onComplete() {
}
})
假设用户 A 发出那条美食的帖子之后,ta 自己没有删除,但是 ta 的关注者 D 在时间线上看到之后,想屏蔽这一条 Status,那么在客户端可以这样执行删除:
// AVStatus target = ...;
target.deleteInBackground()
.subscribe(new Observer<AVNull>() {
@Override
public void onSubscribe(Disposable disposable) {
}
@Override
public void onNext(AVNull v) {
// succeed
}
@Override
public void onError(Throwable throwable) {
// failed
}
@Override
public void onComplete() {
}
})
可以看到,Status 的删除方式都是一样的,区别只在于当前登录用户是否拥有所有权:
- 如果是当前用户自己发布的 Status,会从源头上进行删除;
- 如果只是存在于当前用户的时间线上,则只会从当前用户的时间线上进行删除;
- 其他情况下,当前用户无权进行删除操作,调用会报错;