Skip to content

Commit 0ff9f1c

Browse files
authored
feat: collection search endpoint with condition. (#771)
* build: to v0.20.10 * fix: CollectionEndpoint api pattern * feat: add collection search endpoint in CollectionEndpoint * feat: impl CollectionEndpoint#getCollectionsWithCondition
1 parent 10797d1 commit 0ff9f1c

File tree

8 files changed

+231
-3
lines changed

8 files changed

+231
-3
lines changed

CHANGELOG.MD

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@
22

33
更新日志文档,版本顺序从新到旧,最新版本在最前(上)面。
44

5+
# 0.20.10
6+
7+
- 新增历史记录(收藏)条件查询接口
8+
- 修正根据条目ID查询收藏类型的接口
9+
510
# 0.20.9
611

712
## 问题修复

api/src/main/java/run/ikaros/api/core/collection/EpisodeCollection.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,4 +46,6 @@ public class EpisodeCollection {
4646
private Integer sequence;
4747
@JsonProperty("ep_group")
4848
private EpisodeGroup group;
49+
@JsonProperty("update_time")
50+
private LocalDateTime updateTime;
4951
}
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package run.ikaros.api.core.collection.vo;
2+
3+
import jakarta.annotation.Nullable;
4+
import lombok.Builder;
5+
import lombok.Data;
6+
import run.ikaros.api.store.enums.CollectionCategory;
7+
import run.ikaros.api.store.enums.CollectionType;
8+
9+
@Data
10+
@Builder
11+
public class FindCollectionCondition {
12+
/**
13+
* default is 1.
14+
*/
15+
private Integer page;
16+
/**
17+
* default is 10.
18+
*/
19+
private Integer size;
20+
private CollectionCategory category;
21+
@Nullable
22+
private CollectionType type;
23+
@Nullable
24+
private String time;
25+
/**
26+
* default is false.
27+
*/
28+
private Boolean updateTimeDesc;
29+
}
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
package run.ikaros.api.store.enums;
2+
3+
public enum CollectionCategory {
4+
SUBJECT,
5+
EPISODE;
6+
}

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
version=0.20.9
1+
version=0.20.10

server/src/main/java/run/ikaros/server/core/collection/CollectionEndpoint.java

Lines changed: 52 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,10 @@
1212
import org.springframework.web.reactive.function.server.ServerResponse;
1313
import reactor.core.publisher.Mono;
1414
import run.ikaros.api.constant.OpenApiConst;
15+
import run.ikaros.api.core.collection.vo.FindCollectionCondition;
16+
import run.ikaros.api.store.enums.CollectionCategory;
1517
import run.ikaros.api.store.enums.CollectionType;
18+
import run.ikaros.api.wrap.PagingWrap;
1619
import run.ikaros.server.endpoint.CoreEndpoint;
1720

1821
@Slf4j
@@ -29,7 +32,7 @@ public RouterFunction<ServerResponse> endpoint() {
2932
var tag = OpenApiConst.CORE_VERSION + "/collection";
3033
return SpringdocRouteBuilder.route()
3134

32-
.GET("/collection/subjectId/{id}", this::getTypeBySubjectId,
35+
.GET("/collection/type/subjectId/{id}", this::getTypeBySubjectId,
3336
builder -> builder.operationId("GetTypeBySubjectId")
3437
.tag(tag).description("Get collection type by subject id.")
3538
.parameter(parameterBuilder()
@@ -40,6 +43,35 @@ public RouterFunction<ServerResponse> endpoint() {
4043
.implementation(CollectionType.class))
4144
)
4245

46+
.GET("/collections/condition", this::getCollectionsWithCondition,
47+
builder -> builder.operationId("GetCollectionsWithCondition")
48+
.tag(tag).description("Get collections with conditions.")
49+
.parameter(parameterBuilder()
50+
.name("category").description("Collection category, default is EPISODE.")
51+
.implementation(CollectionCategory.class))
52+
.parameter(parameterBuilder()
53+
.name("type")
54+
.description("Collection type, default is null.")
55+
.implementation(CollectionType.class))
56+
.parameter(parameterBuilder()
57+
.name("page")
58+
.description("第几页,从1开始, 默认为1.")
59+
.implementation(Integer.class))
60+
.parameter(parameterBuilder()
61+
.name("size")
62+
.description("每页条数,默认为10.")
63+
.implementation(Integer.class))
64+
.parameter(parameterBuilder()
65+
.name("time")
66+
.implementation(String.class)
67+
.description("时间范围,格式范围类型: 2000.9-2010.8 或者 单个类型2020.8"))
68+
.parameter(parameterBuilder()
69+
.name("updateTimeDesc")
70+
.implementation(Boolean.class)
71+
.description("是否根据更新时间倒序,默认为 true."))
72+
.response(responseBuilder().implementation(PagingWrap.class))
73+
)
74+
4375
.build();
4476
}
4577

@@ -49,4 +81,23 @@ private Mono<ServerResponse> getTypeBySubjectId(ServerRequest serverRequest) {
4981
return collectionService.findTypeBySubjectId(subjectId)
5082
.flatMap(collectionType -> ServerResponse.ok().bodyValue(collectionType));
5183
}
84+
85+
private Mono<ServerResponse> getCollectionsWithCondition(ServerRequest serverRequest) {
86+
CollectionCategory category = CollectionCategory.valueOf(
87+
serverRequest.queryParam("category").orElse(CollectionCategory.EPISODE.name()));
88+
CollectionType type = serverRequest.queryParam("type").map(CollectionType::valueOf)
89+
.orElse(null);
90+
int page = serverRequest.queryParam("page").map(Integer::parseInt).orElse(1);
91+
int size = serverRequest.queryParam("size").map(Integer::parseInt).orElse(10);
92+
String time = serverRequest.queryParam("time").orElse(null);
93+
boolean updateTimeDesc = serverRequest.queryParam("updateTimeDesc")
94+
.map(Boolean::parseBoolean).orElse(false);
95+
FindCollectionCondition condition = FindCollectionCondition
96+
.builder().page(page).size(size)
97+
.category(category).type(type).time(time).updateTimeDesc(updateTimeDesc)
98+
.build();
99+
100+
return collectionService.listCollectionsByCondition(condition)
101+
.flatMap(pagingWrap -> ServerResponse.ok().bodyValue(pagingWrap));
102+
}
52103
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
package run.ikaros.server.core.collection;
22

33
import reactor.core.publisher.Mono;
4+
import run.ikaros.api.core.collection.vo.FindCollectionCondition;
45
import run.ikaros.api.store.enums.CollectionType;
6+
import run.ikaros.api.wrap.PagingWrap;
57

68
public interface CollectionService {
79
Mono<CollectionType> findTypeBySubjectId(Long subjectId);
10+
11+
Mono<PagingWrap> listCollectionsByCondition(FindCollectionCondition condition);
812
}

server/src/main/java/run/ikaros/server/core/collection/DefaultCollectionService.java

Lines changed: 132 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,52 @@
11
package run.ikaros.server.core.collection;
22

3+
import static run.ikaros.api.infra.utils.ReactiveBeanUtils.copyProperties;
4+
5+
import java.time.LocalDateTime;
6+
import java.time.Year;
7+
import java.util.Objects;
38
import lombok.extern.slf4j.Slf4j;
9+
import org.springframework.data.domain.PageRequest;
10+
import org.springframework.data.domain.Sort;
11+
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
12+
import org.springframework.data.relational.core.query.Criteria;
13+
import org.springframework.data.relational.core.query.Query;
414
import org.springframework.stereotype.Service;
515
import org.springframework.util.Assert;
16+
import reactor.core.publisher.Flux;
617
import reactor.core.publisher.Mono;
18+
import run.ikaros.api.core.collection.EpisodeCollection;
19+
import run.ikaros.api.core.collection.SubjectCollection;
20+
import run.ikaros.api.core.collection.vo.FindCollectionCondition;
21+
import run.ikaros.api.infra.utils.StringUtils;
22+
import run.ikaros.api.store.enums.CollectionCategory;
723
import run.ikaros.api.store.enums.CollectionType;
24+
import run.ikaros.api.wrap.PagingWrap;
825
import run.ikaros.server.core.user.UserService;
26+
import run.ikaros.server.store.entity.EpisodeCollectionEntity;
927
import run.ikaros.server.store.entity.SubjectCollectionEntity;
28+
import run.ikaros.server.store.repository.EpisodeCollectionRepository;
1029
import run.ikaros.server.store.repository.SubjectCollectionRepository;
1130

1231
@Slf4j
1332
@Service
1433
public class DefaultCollectionService implements CollectionService {
1534
private final SubjectCollectionRepository subjectCollectionRepository;
35+
private final EpisodeCollectionRepository episodeCollectionRepository;
1636

1737
private final UserService userService;
38+
private final R2dbcEntityTemplate template;
1839

40+
/**
41+
* Construct a {@link CollectionService} instance.
42+
*/
1943
public DefaultCollectionService(SubjectCollectionRepository subjectCollectionRepository,
20-
UserService userService) {
44+
EpisodeCollectionRepository episodeCollectionRepository,
45+
UserService userService, R2dbcEntityTemplate template) {
2146
this.subjectCollectionRepository = subjectCollectionRepository;
47+
this.episodeCollectionRepository = episodeCollectionRepository;
2248
this.userService = userService;
49+
this.template = template;
2350
}
2451

2552
@Override
@@ -29,4 +56,108 @@ public Mono<CollectionType> findTypeBySubjectId(Long subjectId) {
2956
.flatMap(uid -> subjectCollectionRepository.findByUserIdAndSubjectId(uid, subjectId))
3057
.map(SubjectCollectionEntity::getType);
3158
}
59+
60+
@Override
61+
public Mono<PagingWrap> listCollectionsByCondition(FindCollectionCondition condition) {
62+
Assert.notNull(condition, "'condition' must not null.");
63+
final Integer page = condition.getPage();
64+
Assert.isTrue(page > 0, "'page' must gt 0.");
65+
final Integer size = condition.getSize();
66+
Assert.isTrue(size > 0, "'size' must gt 0.");
67+
final PageRequest pageRequest = PageRequest.of(page - 1, size);
68+
69+
Criteria criteria = Criteria.empty();
70+
71+
if (CollectionCategory.SUBJECT == condition.getCategory()
72+
&& !Objects.isNull(condition.getType())) {
73+
criteria = criteria.and("type").is(condition.getType());
74+
}
75+
76+
// 范围查询更新时间,用于查询指定时间段的收藏纪录
77+
String time = condition.getTime();
78+
if (CollectionCategory.EPISODE == condition.getCategory()
79+
&& StringUtils.isNotBlank(time)
80+
) {
81+
if (time.indexOf('-') > 0) {
82+
// 日期范围,例如;2000.9-2010.8
83+
String[] split = time.split("-");
84+
String first = split[0];
85+
String second = split[1];
86+
LocalDateTime startTime;
87+
if (first.indexOf(".") > 0) {
88+
String[] split1 = first.split("\\.");
89+
startTime =
90+
Year.parse(split1[0]).atMonth(Integer.parseInt(split1[1])).atDay(1)
91+
.atStartOfDay();
92+
} else {
93+
startTime = Year.parse(first).atMonth(1).atDay(1).atStartOfDay();
94+
}
95+
LocalDateTime endTime;
96+
if (second.indexOf(".") > 0) {
97+
String[] split2 = second.split("\\.");
98+
endTime =
99+
Year.parse(split2[0]).atMonth(Integer.parseInt(split2[1])).atDay(1)
100+
.atStartOfDay().plusMonths(1);
101+
} else {
102+
endTime = Year.parse(second).atDay(1).atStartOfDay().plusMonths(1);
103+
}
104+
criteria = criteria.and(Criteria.where("update_time").between(startTime, endTime));
105+
} else {
106+
// 单个类型,例如:2020.8
107+
if (time.indexOf('.') > 0) {
108+
String[] split = time.split("\\.");
109+
LocalDateTime startTime =
110+
Year.parse(split[0]).atMonth(Integer.parseInt(split[1])).atDay(1)
111+
.atStartOfDay();
112+
criteria = criteria.and(
113+
Criteria.where("update_time").between(startTime, startTime.plusMonths(1)));
114+
} else {
115+
LocalDateTime startTime = Year.parse(time).atMonth(1).atDay(1).atStartOfDay();
116+
criteria = criteria.and(
117+
Criteria.where("update_time").between(startTime, startTime.plusYears(1)));
118+
}
119+
}
120+
}
121+
122+
Query query = Query.query(criteria);
123+
124+
if (CollectionCategory.EPISODE == condition.getCategory()
125+
&& condition.getUpdateTimeDesc()) {
126+
query = query.sort(Sort.by(Sort.Order.desc("update_time").nullsLast()));
127+
}
128+
129+
if (CollectionCategory.EPISODE == condition.getCategory()) {
130+
query = query
131+
.sort(Sort.by(
132+
condition.getUpdateTimeDesc() ? Sort.Order.desc("update_time").nullsLast()
133+
: Sort.Order.asc("update_time").nullsLast()));
134+
}
135+
136+
query = query.with(pageRequest);
137+
138+
Mono<Long> countMono;
139+
if (CollectionCategory.EPISODE == condition.getCategory()) {
140+
Flux<EpisodeCollectionEntity> episodeCollectionEntityFlux =
141+
template.select(query, EpisodeCollectionEntity.class);
142+
countMono = template.count(query, EpisodeCollectionEntity.class);
143+
return episodeCollectionEntityFlux.map(EpisodeCollectionEntity::getId)
144+
.flatMap(episodeCollectionRepository::findById)
145+
.flatMap(entity -> copyProperties(entity, new EpisodeCollection()))
146+
.collectList()
147+
.flatMap(episodeCollections -> countMono
148+
.map(count -> new PagingWrap<>(page, size, count, episodeCollections)));
149+
} else if (CollectionCategory.SUBJECT == condition.getCategory()) {
150+
Flux<SubjectCollectionEntity> subjectCollectionEntityFlux =
151+
template.select(query, SubjectCollectionEntity.class);
152+
countMono = template.count(query, SubjectCollectionEntity.class);
153+
return subjectCollectionEntityFlux.map(SubjectCollectionEntity::getId)
154+
.flatMap(subjectCollectionRepository::findById)
155+
.flatMap(entity -> copyProperties(entity, new SubjectCollection()))
156+
.collectList()
157+
.flatMap(subjects -> countMono
158+
.map(count -> new PagingWrap<>(page, size, count, subjects)));
159+
} else {
160+
return Mono.empty();
161+
}
162+
}
32163
}

0 commit comments

Comments
 (0)