一. 概述
1.1 业务诉求
想象一个向用户展示数据的下拉列表,数据的来源有多种方式。支持在每一页都按固定的数据源类型及顺序展示。
1.2 业务场景
- up主的主页展示带货商品列表
商品来源有多种:up主自选、官方推荐的、根据up主风格AI推荐 - 用户推荐视频列表
视频来源有多种:用户关注、用户画像推荐、播放排行榜
二、方案
每一页的每个位置的数据,有数据源类型和顺序的差异,将每个配置定义为一个坑位。即每一页有pageSize个坑位。
2.1 后台配置
在后台定义每页所有坑位的数据源类型,每页坑位配置的合集定义为坑位模版:
struct{
1:i32: minPageSize //每页最小的数量
2:list<string>: slotTypes //所有坑位的类型
}
2.2 数据查询
- 坑位模版groupBy数据源类型,获取单页每种数据源的count
- 每一个数据源类型有一个分页查询接口,基于分页接口封装为游标(pageSize=坑位count)
- 按坑位模版的模型,分别从游标中获取元素充填:如果元素不够或元素被filter则后面的元素自动顶上,如果过滤后的所有数据源数量size<minPageSize,则继续取下一页,直到满足条件
注意其中的细节:
- 分页参数offset: 包含还有数据dataSource和pageNo信息,第一次传空后续用上一次返回的offset
- 单页的数据大小: [minPageSize, minPageSize+pageSize]
- 某坑位元素被过滤后,尽管下一页还有数据,但该位置仍被后续其他类型数据占用。
游标定义参考:
public class PageCursorList<T> {
private int readCursor;
@NonNull
private final Function<Integer,List<T>> pageFetcher;
private int pageNo = 1 ;
private final int pageSize;
protected List<T> list;
public PageCursorList(int pageSize,Function<Integer,List<T>> pageFetcher){
Preconditions.checkArgument(pageSize > 0,"pageSize must gt 0");
this.pageSize = pageSize;
this.pageFetcher = pageFetcher;
//first page
this.list = Optional.ofNullable(pageFetcher.apply(1)).orElse(Collections.emptyList());
}
public boolean hasNext() {
boolean currentPageHasNext = readCursor < list.size();
if(currentPageHasNext || list.size() < pageSize){
return currentPageHasNext;
}
nextPage();
return readCursor < list.size();
}
public T getNext() {
if (readCursor < list.size()) {
return list.get(readCursor++);
}
return null;
}
private void nextPage(){
pageNo++;
this.list = Optional.ofNullable(pageFetcher.apply(pageNo)).orElse(Collections.emptyList());
this.readCursor = 0;
}
}
三、扩展
- 基于用户画像或标签,配置多套坑位模版,实现精准运营
- 配置多套坑位模版,实现AB
在用户查询时再套一层即可: