feat: add more query predicates for post list (#2436)

#### What type of PR is this?
/kind improvement
/area core
/milestone 2.0

#### What this PR does / why we need it:
文章列表新增更过的查询条件
- publishPhase 状态
- visible 可见性
- keyword 关键词
新增排序(sort)(可逆序 sortOrder)
- 创建时间(默认创建时间逆序)
- 发布时间

关键词过滤暂不管文章内容,否则需要查询所有文章内容判断是否包含字符串
排序暂无法支持评论数量和阅读量,这两个属性属于文章统计需要 #2430 的支撑
#### Which issue(s) this PR fixes:

Fixes #2424

#### Special notes for your reviewer:
/cc @halo-dev/sig-halo 
#### Does this PR introduce a user-facing change?

```release-note
None
```
pull/2456/head
guqing 2022-09-22 14:14:12 +08:00 committed by GitHub
parent eee81e78f1
commit bfbc4ec70a
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 188 additions and 6 deletions

View File

@ -3,6 +3,7 @@ package run.halo.app.content;
import io.swagger.v3.oas.annotations.media.Schema;
import java.util.List;
import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.springframework.lang.Nullable;
import org.springframework.util.MultiValueMap;
import run.halo.app.core.extension.Post;
@ -39,7 +40,41 @@ public class PostQuery extends IListRequest.QueryListRequest {
}
@Nullable
Set<String> listToSet(List<String> param) {
public Post.PostPhase getPublishPhase() {
String publishPhase = queryParams.getFirst("publishPhase");
return Post.PostPhase.from(publishPhase);
}
@Nullable
public Post.VisibleEnum getVisible() {
String visible = queryParams.getFirst("visible");
return Post.VisibleEnum.from(visible);
}
@Nullable
@Schema(description = "Posts filtered by keyword.")
public String getKeyword() {
return StringUtils.defaultIfBlank(queryParams.getFirst("keyword"), null);
}
@Schema(description = "Post collation.")
public PostSorter getSort() {
String sort = queryParams.getFirst("sort");
return PostSorter.convertFrom(sort);
}
@Schema(description = "ascending order If it is true; otherwise, it is in descending order.")
public Boolean getSortOrder() {
String sortOrder = queryParams.getFirst("sortOrder");
return convertBooleanOrNull(sortOrder);
}
@Nullable
private Set<String> listToSet(List<String> param) {
return param == null ? null : Set.copyOf(param);
}
private Boolean convertBooleanOrNull(String value) {
return StringUtils.isBlank(value) ? null : Boolean.parseBoolean(value);
}
}

View File

@ -0,0 +1,79 @@
package run.halo.app.content;
import java.time.Instant;
import java.util.Comparator;
import java.util.Objects;
import java.util.function.Function;
import org.springframework.util.comparator.Comparators;
import run.halo.app.core.extension.Post;
/**
* A sorter for {@link Post}.
*
* @author guqing
* @since 2.0.0
*/
public enum PostSorter {
PUBLISH_TIME,
CREATE_TIME;
static final Function<Post, String> name = post -> post.getMetadata().getName();
/**
* Converts {@link Comparator} from {@link PostSorter} and ascending.
*
* @param sorter a {@link PostSorter}
* @param ascending ascending if true, otherwise descending
* @return a {@link Comparator} of {@link Post}
*/
public static Comparator<Post> from(PostSorter sorter, Boolean ascending) {
if (Objects.equals(true, ascending)) {
return from(sorter);
}
return from(sorter).reversed();
}
/**
* Converts {@link Comparator} from {@link PostSorter}.
*
* @param sorter a {@link PostSorter}
* @return a {@link Comparator} of {@link Post}
*/
public static Comparator<Post> from(PostSorter sorter) {
if (sorter == null) {
return defaultComparator();
}
if (CREATE_TIME.equals(sorter)) {
Function<Post, Instant> comparatorFunc =
post -> post.getMetadata().getCreationTimestamp();
return Comparator.comparing(comparatorFunc)
.thenComparing(name);
}
if (PUBLISH_TIME.equals(sorter)) {
Function<Post, Instant> comparatorFunc =
post -> post.getSpec().getPublishTime();
return Comparator.comparing(comparatorFunc, Comparators.nullsLow())
.thenComparing(name);
}
throw new IllegalArgumentException("Unsupported sort value: " + sorter);
}
static PostSorter convertFrom(String sort) {
for (PostSorter sorter : values()) {
if (sorter.name().equalsIgnoreCase(sort)) {
return sorter;
}
}
return null;
}
static Comparator<Post> defaultComparator() {
Function<Post, Instant> createTime =
post -> post.getMetadata().getCreationTimestamp();
return Comparator.comparing(createTime)
.thenComparing(name)
.reversed();
}
}

View File

@ -10,6 +10,7 @@ import java.util.List;
import java.util.Objects;
import java.util.function.Function;
import java.util.function.Predicate;
import org.apache.commons.lang3.StringUtils;
import org.springframework.security.core.context.ReactiveSecurityContextHolder;
import org.springframework.security.core.context.SecurityContext;
import org.springframework.stereotype.Component;
@ -22,6 +23,7 @@ import run.halo.app.content.ListedPost;
import run.halo.app.content.PostQuery;
import run.halo.app.content.PostRequest;
import run.halo.app.content.PostService;
import run.halo.app.content.PostSorter;
import run.halo.app.core.extension.Category;
import run.halo.app.core.extension.Post;
import run.halo.app.core.extension.Snapshot;
@ -40,8 +42,6 @@ import run.halo.app.infra.ConditionStatus;
*/
@Component
public class PostServiceImpl implements PostService {
private static final Comparator<Post> DEFAULT_POST_COMPARATOR =
Comparator.comparing(post -> post.getMetadata().getCreationTimestamp());
private final ContentService contentService;
private final ReactiveExtensionClient client;
@ -52,8 +52,10 @@ public class PostServiceImpl implements PostService {
@Override
public Mono<ListResult<ListedPost>> listPost(PostQuery query) {
Comparator<Post> comparator =
PostSorter.from(query.getSort(), query.getSortOrder());
return client.list(Post.class, postListPredicate(query),
DEFAULT_POST_COMPARATOR.reversed(), query.getPage(), query.getSize())
comparator, query.getPage(), query.getSize())
.flatMap(listResult -> Flux.fromStream(
listResult.get().map(this::getListedPost)
)
@ -70,6 +72,40 @@ public class PostServiceImpl implements PostService {
contains(query.getCategories(), post.getSpec().getCategories())
&& contains(query.getTags(), post.getSpec().getTags())
&& contains(query.getContributors(), post.getStatus().getContributors());
String keyword = query.getKeyword();
if (keyword != null) {
paramPredicate = paramPredicate.and(post -> {
String excerpt = post.getStatusOrDefault().getExcerpt();
return StringUtils.containsIgnoreCase(excerpt, keyword)
|| StringUtils.containsIgnoreCase(post.getSpec().getSlug(), keyword)
|| StringUtils.containsIgnoreCase(post.getSpec().getTitle(), keyword);
});
}
Post.PostPhase publishPhase = query.getPublishPhase();
if (publishPhase != null) {
paramPredicate = paramPredicate.and(post -> {
if (Post.PostPhase.PENDING_APPROVAL.equals(publishPhase)) {
return !post.isPublished()
&& Post.PostPhase.PENDING_APPROVAL.name()
.equalsIgnoreCase(post.getStatusOrDefault().getPhase());
}
// published
if (Post.PostPhase.PUBLISHED.equals(publishPhase)) {
return post.isPublished();
}
// draft
return !post.isPublished();
});
}
Post.VisibleEnum visible = query.getVisible();
if (visible != null) {
paramPredicate =
paramPredicate.and(post -> visible.equals(post.getSpec().getVisible()));
}
Predicate<Post> predicate = labelAndFieldSelectorToPredicate(query.getLabelSelector(),
query.getFieldSelector());
return predicate.and(paramPredicate);

View File

@ -130,6 +130,8 @@ public class Post extends AbstractExtension {
private Boolean inProgress;
private Integer commentsCount;
private List<String> contributors;
@JsonIgnore
@ -153,13 +155,43 @@ public class Post extends AbstractExtension {
public enum PostPhase {
DRAFT,
PENDING_APPROVAL,
PUBLISHED
PUBLISHED;
/**
* Convert string value to {@link PostPhase}.
*
* @param value enum value string
* @return {@link PostPhase} if found, otherwise null
*/
public static PostPhase from(String value) {
for (PostPhase phase : PostPhase.values()) {
if (phase.name().equalsIgnoreCase(value)) {
return phase;
}
}
return null;
}
}
public enum VisibleEnum {
PUBLIC,
INTERNAL,
PRIVATE
PRIVATE;
/**
* Convert value string to {@link VisibleEnum}.
*
* @param value enum value string
* @return {@link VisibleEnum} if found, otherwise null
*/
public static VisibleEnum from(String value) {
for (VisibleEnum visible : VisibleEnum.values()) {
if (visible.name().equalsIgnoreCase(value)) {
return visible;
}
}
return null;
}
}
@Data