Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1a8e81bb5b | |||
| 0e4184057a |
@@ -2,6 +2,13 @@
|
||||
|
||||
> 基于原作者v1.2.2代码修改
|
||||
|
||||
#### v1.2.2-3
|
||||
1. 标题、描述接入搜索引擎;新增详情页
|
||||
2. 控制台新增必填项;修改字数上限、时间格式
|
||||
|
||||
#### v1.2.2-2
|
||||
1. 新增类别接口`/apis/api.douban.moony.la/v1alpha1/doubanmovies/-/types`(基于原作者v1.2.5代码)
|
||||
|
||||
#### v1.2.2-1
|
||||
1. 修改前台页面路由为/books
|
||||
2. 控制台类别value不再限制
|
||||
|
||||
@@ -144,6 +144,28 @@
|
||||
"tags" : [ "api.douban.moony.la/v1alpha1/DoubanMovie" ]
|
||||
}
|
||||
},
|
||||
"/apis/api.douban.moony.la/v1alpha1/doubanmovies/-/types" : {
|
||||
"get" : {
|
||||
"description" : "List all douban types.",
|
||||
"operationId" : "ListTypes",
|
||||
"responses" : {
|
||||
"default" : {
|
||||
"content" : {
|
||||
"*/*" : {
|
||||
"schema" : {
|
||||
"type" : "array",
|
||||
"items" : {
|
||||
"$ref" : "#/components/schemas/DoubanTypeVo"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"description" : "default response"
|
||||
}
|
||||
},
|
||||
"tags" : [ "api.douban.moony.la/v1alpha1/DoubanMovie" ]
|
||||
}
|
||||
},
|
||||
"/apis/api.douban.moony.la/v1alpha1/doubanmovies/-/getDoubanDetail" : {
|
||||
"get" : {
|
||||
"description" : "getDoubanDetail.",
|
||||
@@ -851,6 +873,21 @@
|
||||
}
|
||||
}
|
||||
},
|
||||
"DoubanTypeVo" : {
|
||||
"type" : "object",
|
||||
"properties" : {
|
||||
"doubanCount" : {
|
||||
"type" : "integer",
|
||||
"format" : "int32"
|
||||
},
|
||||
"key" : {
|
||||
"type" : "string"
|
||||
},
|
||||
"name" : {
|
||||
"type" : "string"
|
||||
}
|
||||
}
|
||||
},
|
||||
"JsonPatch" : {
|
||||
"minItems" : 1,
|
||||
"uniqueItems" : true,
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
version=1.2.2-1
|
||||
version=1.2.2-3
|
||||
|
||||
@@ -2,8 +2,10 @@ package la.moony.douban;
|
||||
|
||||
import io.swagger.v3.oas.annotations.enums.ParameterIn;
|
||||
import la.moony.douban.extension.DoubanMovie;
|
||||
import la.moony.douban.finders.DoubanFinder;
|
||||
import la.moony.douban.service.DoubanService;
|
||||
import la.moony.douban.vo.DoubanMovieVo;
|
||||
import la.moony.douban.vo.DoubanTypeVo;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springdoc.webflux.core.fn.SpringdocRouteBuilder;
|
||||
import org.springframework.stereotype.Component;
|
||||
@@ -25,9 +27,12 @@ public class DoubanEndpoint implements CustomEndpoint {
|
||||
|
||||
private final DoubanService doubanService;
|
||||
|
||||
private final DoubanFinder doubanFinder;
|
||||
|
||||
public DoubanEndpoint(DoubanService doubanService) {
|
||||
|
||||
public DoubanEndpoint(DoubanService doubanService, DoubanFinder doubanFinder) {
|
||||
this.doubanService = doubanService;
|
||||
this.doubanFinder = doubanFinder;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -43,6 +48,13 @@ public class DoubanEndpoint implements CustomEndpoint {
|
||||
);
|
||||
DoubanMovieQuery.buildParameters(builder);
|
||||
})
|
||||
.GET("doubanmovies/-/types", this::ListTypes,
|
||||
builder -> builder.operationId("ListTypes")
|
||||
.description("List all douban types.")
|
||||
.tag(doubanMovieTag)
|
||||
.response(responseBuilder()
|
||||
.implementationArray(DoubanTypeVo.class)
|
||||
))
|
||||
.GET("doubanmovies/-/genres", this::ListGenres,
|
||||
builder -> builder.operationId("ListGenres")
|
||||
.description("List all douban genres.")
|
||||
@@ -87,6 +99,12 @@ public class DoubanEndpoint implements CustomEndpoint {
|
||||
.flatMap(result -> ServerResponse.ok().bodyValue(result));
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> ListTypes(ServerRequest request) {
|
||||
return doubanFinder.listAllType()
|
||||
.collectList()
|
||||
.flatMap(result -> ServerResponse.ok().bodyValue(result));
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> getDoubanDetail(ServerRequest request) {
|
||||
String url = request.queryParam("url").orElse(null);
|
||||
return doubanService.getDoubanDetail(url)
|
||||
|
||||
@@ -51,6 +51,10 @@ public class DoubanPlugin extends BasePlugin {
|
||||
.setName("spec.name")
|
||||
.setIndexFunc(
|
||||
simpleAttribute(DoubanMovie.class, doubanMovie -> doubanMovie.getSpec().getName())));
|
||||
indexSpecs.add(new IndexSpec()
|
||||
.setName("spec.cardSubtitle")
|
||||
.setIndexFunc(simpleAttribute(DoubanMovie.class,
|
||||
doubanMovie -> doubanMovie.getSpec().getCardSubtitle())));
|
||||
indexSpecs.add(new IndexSpec()
|
||||
.setName("spec.id")
|
||||
.setIndexFunc(
|
||||
|
||||
@@ -29,9 +29,18 @@ public class DoubanRouter {
|
||||
RouterFunction<ServerResponse> friendTemplateRoute() {
|
||||
|
||||
return RouterFunctions.route().GET("/books",this::handlerFunction)
|
||||
.GET("/books/{doubanMovieName:\\S+}", this::doubanMovieDetail)
|
||||
.build();
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> doubanMovieDetail(ServerRequest request) {
|
||||
String doubanMovieName = request.pathVariable("doubanMovieName");
|
||||
return templateNameResolver.resolveTemplateNameOrDefault(request.exchange(), "douban-detail")
|
||||
.flatMap(templateName -> ServerResponse.ok().render(templateName,
|
||||
java.util.Map.of("douban", doubanFinder.get(doubanMovieName),
|
||||
"title", getDoubanTitle())));
|
||||
}
|
||||
|
||||
private Mono<ServerResponse> handlerFunction(ServerRequest request) {
|
||||
String type = request.queryParam("type")
|
||||
.filter(StringUtils::isNotBlank)
|
||||
|
||||
@@ -0,0 +1,59 @@
|
||||
package la.moony.douban.search;
|
||||
|
||||
import static la.moony.douban.search.DoubanMovieHaloDocumentsProvider.DOUBAN_MOVIE_DOCUMENT_TYPE;
|
||||
|
||||
import java.time.Instant;
|
||||
import java.util.List;
|
||||
import la.moony.douban.extension.DoubanMovie;
|
||||
import lombok.NonNull;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.core.convert.converter.Converter;
|
||||
import org.springframework.stereotype.Component;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.infra.ExternalUrlSupplier;
|
||||
import run.halo.app.search.HaloDocument;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class DoubanMovieDocumentConverter implements Converter<DoubanMovie, Mono<HaloDocument>> {
|
||||
|
||||
private final ExternalUrlSupplier externalUrlSupplier;
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public Mono<HaloDocument> convert(DoubanMovie movie) {
|
||||
var spec = movie.getSpec();
|
||||
String title = StringUtils.defaultIfBlank(spec == null ? null : spec.getName(), "豆瓣记录");
|
||||
String description = StringUtils.defaultIfBlank(
|
||||
spec == null ? null : spec.getCardSubtitle(), "无描述");
|
||||
var document = new HaloDocument();
|
||||
document.setMetadataName(movie.getMetadata().getName());
|
||||
document.setType(DOUBAN_MOVIE_DOCUMENT_TYPE);
|
||||
document.setId(haloDocId(movie));
|
||||
document.setTitle(title);
|
||||
document.setDescription(description);
|
||||
document.setContent(String.join(" ", title, description));
|
||||
document.setExposed(true);
|
||||
document.setTags(List.of());
|
||||
document.setOwnerName("豆瓣");
|
||||
document.setPublished(true);
|
||||
document.setPermalink(externalUrlSupplier.get()
|
||||
.resolve("books/" + movie.getMetadata().getName()).toString());
|
||||
document.setCreationTimestamp(creationTimestamp(movie));
|
||||
document.setUpdateTimestamp(creationTimestamp(movie));
|
||||
return Mono.just(document);
|
||||
}
|
||||
|
||||
String haloDocId(DoubanMovie movie) {
|
||||
return DOUBAN_MOVIE_DOCUMENT_TYPE + '-' + movie.getMetadata().getName();
|
||||
}
|
||||
|
||||
private Instant creationTimestamp(DoubanMovie movie) {
|
||||
if (movie.getFaves() != null && movie.getFaves().getCreateTime() != null) {
|
||||
return movie.getFaves().getCreateTime();
|
||||
}
|
||||
return movie.getMetadata().getCreationTimestamp() == null
|
||||
? Instant.EPOCH : movie.getMetadata().getCreationTimestamp();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package la.moony.douban.search;
|
||||
|
||||
import la.moony.douban.extension.DoubanMovie;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.stereotype.Component;
|
||||
import reactor.core.publisher.Flux;
|
||||
import reactor.core.publisher.Mono;
|
||||
import run.halo.app.extension.ListOptions;
|
||||
import run.halo.app.extension.ListResult;
|
||||
import run.halo.app.extension.PageRequest;
|
||||
import run.halo.app.extension.PageRequestImpl;
|
||||
import run.halo.app.extension.ReactiveExtensionClient;
|
||||
import run.halo.app.extension.router.selector.FieldSelector;
|
||||
import run.halo.app.search.HaloDocument;
|
||||
import run.halo.app.search.HaloDocumentsProvider;
|
||||
|
||||
import static run.halo.app.extension.index.query.QueryFactory.isNull;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class DoubanMovieHaloDocumentsProvider implements HaloDocumentsProvider {
|
||||
|
||||
public static final String DOUBAN_MOVIE_DOCUMENT_TYPE = "doubanmovie.douban.moony.la";
|
||||
private static final int PAGE_SIZE = 200;
|
||||
|
||||
private final ReactiveExtensionClient client;
|
||||
private final DoubanMovieDocumentConverter converter;
|
||||
|
||||
@Override
|
||||
public Flux<HaloDocument> fetchAll() {
|
||||
var options = new ListOptions();
|
||||
options.setFieldSelector(FieldSelector.of(isNull("metadata.deletionTimestamp")));
|
||||
var pageRequest = PageRequestImpl.of(1, PAGE_SIZE,
|
||||
Sort.by("metadata.creationTimestamp", "metadata.name"));
|
||||
return client.listBy(DoubanMovie.class, options, pageRequest)
|
||||
.expand(result -> result.hasNext()
|
||||
? client.listBy(DoubanMovie.class, options,
|
||||
PageRequestImpl.of(result.getPage() + 1, result.getSize(), pageRequest.getSort()))
|
||||
: Mono.empty())
|
||||
.flatMap(result -> Flux.fromStream(result.get()))
|
||||
.flatMap(converter::convert);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getType() {
|
||||
return DOUBAN_MOVIE_DOCUMENT_TYPE;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
package la.moony.douban.search;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Set;
|
||||
import la.moony.douban.extension.DoubanMovie;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.context.ApplicationEventPublisher;
|
||||
import org.springframework.stereotype.Component;
|
||||
import run.halo.app.extension.ExtensionClient;
|
||||
import run.halo.app.extension.ExtensionUtil;
|
||||
import run.halo.app.extension.controller.Controller;
|
||||
import run.halo.app.extension.controller.ControllerBuilder;
|
||||
import run.halo.app.extension.controller.Reconciler;
|
||||
import run.halo.app.search.event.HaloDocumentAddRequestEvent;
|
||||
import run.halo.app.search.event.HaloDocumentDeleteRequestEvent;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class DoubanMovieSearchReconciler implements Reconciler<Reconciler.Request> {
|
||||
|
||||
private static final String FINALIZER = "douban-movie-search-protection";
|
||||
private final ApplicationEventPublisher eventPublisher;
|
||||
private final ExtensionClient client;
|
||||
private final DoubanMovieDocumentConverter converter;
|
||||
|
||||
@Override
|
||||
public Result reconcile(Request request) {
|
||||
client.fetch(DoubanMovie.class, request.name()).ifPresent(movie -> {
|
||||
if (ExtensionUtil.isDeleted(movie)) {
|
||||
if (ExtensionUtil.removeFinalizers(movie.getMetadata(), Set.of(FINALIZER))) {
|
||||
eventPublisher.publishEvent(new HaloDocumentDeleteRequestEvent(this,
|
||||
List.of(converter.haloDocId(movie))));
|
||||
client.update(movie);
|
||||
}
|
||||
return;
|
||||
}
|
||||
ExtensionUtil.addFinalizers(movie.getMetadata(), Set.of(FINALIZER));
|
||||
var document = converter.convert(movie).blockOptional().orElseThrow();
|
||||
eventPublisher.publishEvent(new HaloDocumentAddRequestEvent(this, List.of(document)));
|
||||
client.update(movie);
|
||||
});
|
||||
return Result.doNotRetry();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Controller setupWith(ControllerBuilder builder) {
|
||||
return builder.extension(new DoubanMovie()).workerCount(1).build();
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,7 @@ metadata:
|
||||
rbac.authorization.halo.run/display-name: "查看朋友圈提交模板"
|
||||
rules:
|
||||
- apiGroups: [ "api.douban.moony.la" ]
|
||||
resources: [ "doubanmovies","doubanmovies/genres","doubanmovies/getDoubanDetail"]
|
||||
resources: [ "doubanmovies","doubanmovies/types","doubanmovies/genres","doubanmovies/getDoubanDetail"]
|
||||
verbs: [ "get", "list" ]
|
||||
|
||||
---
|
||||
|
||||
@@ -182,8 +182,8 @@ class HALO_DOUBAN {
|
||||
}${
|
||||
movie.spec.year > 0 ? " · " + movie.spec.year : ""
|
||||
}</div><div class="db--title"><a href="${
|
||||
movie.spec.link
|
||||
}" target="_blank">${movie.spec.name}</a></div>
|
||||
`/books/${movie.metadata.name}`
|
||||
}">${movie.spec.name}</a></div>
|
||||
|
||||
</div>`;
|
||||
})
|
||||
@@ -212,8 +212,8 @@ class HALO_DOUBAN {
|
||||
}${
|
||||
item.spec.year > 0 ? " · " + item.spec.year : ""
|
||||
}</div><div class="db--title"><a href="${
|
||||
item.spec.link
|
||||
}" target="_blank">${item.spec.name}</a></div>
|
||||
`/books/${item.metadata.name}`
|
||||
}">${item.spec.name}</a></div>
|
||||
</div>
|
||||
</div>`;
|
||||
})
|
||||
@@ -294,4 +294,4 @@ class HALO_DOUBAN {
|
||||
}
|
||||
}
|
||||
|
||||
new HALO_DOUBAN();
|
||||
new HALO_DOUBAN();
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="zh-CN" xmlns:th="https://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<title th:text="${douban != null ? douban.spec.name : '豆瓣记录'}">豆瓣记录</title>
|
||||
<link rel="stylesheet" th:href="@{/plugins/plugin-douban/assets/static/db.min.css}">
|
||||
</head>
|
||||
<body>
|
||||
<main class="layoutSingleColumn layoutSingleColumn--wide u-paddingTop50">
|
||||
<section class="doulist-item" th:if="${douban != null}">
|
||||
<div class="doulist-subject">
|
||||
<div class="doulist-post"><img class="db--image" th:src="${douban.spec.poster}" alt=""></div>
|
||||
<div class="doulist-content">
|
||||
<div class="doulist-title" th:text="${douban.spec.name}">标题</div>
|
||||
<div class="abstract" th:text="${douban.spec.cardSubtitle}">描述</div>
|
||||
<p th:text="${douban.faves.remark}">备注</p>
|
||||
<a th:href="${douban.spec.link}" target="_blank" rel="external nofollow">查看豆瓣原页</a>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
<p th:if="${douban == null}">未找到该条豆瓣记录。</p>
|
||||
</main>
|
||||
</body>
|
||||
</html>
|
||||
@@ -83,9 +83,9 @@
|
||||
|
||||
</main>
|
||||
<footer class="site--footer">
|
||||
<script th:src="@{/plugins/plugin-douban/assets/static/db.min.js?v={version}(version=${version})}"></script>
|
||||
<script th:src="@{/plugins/plugin-douban/assets/static/db.js?v={version}(version=${version})}"></script>
|
||||
</footer>
|
||||
|
||||
</body>
|
||||
|
||||
</html>
|
||||
</html>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { ref, computed, nextTick, watch} from "vue";
|
||||
import cloneDeep from "lodash.clonedeep";
|
||||
import {doubanCoreApiClient} from "@/api";
|
||||
import type { DoubanMovie } from "@/api/generated";
|
||||
import {toDatetimeLocal, toISOString} from "@/utils/date";
|
||||
import {toDateInput, toISOString} from "@/utils/date";
|
||||
|
||||
const props = withDefaults(
|
||||
defineProps<{
|
||||
@@ -55,6 +55,8 @@ const saving = ref<boolean>(false);
|
||||
const formVisible = ref(false);
|
||||
const createTime = ref<string | undefined>(undefined);
|
||||
|
||||
const getToday = () => toDateInput(new Date());
|
||||
|
||||
|
||||
const isUpdateMode = computed(() => {
|
||||
return !!formState.value.metadata.creationTimestamp;
|
||||
@@ -73,6 +75,9 @@ const onVisibleChange = (visible: boolean) => {
|
||||
|
||||
const handleResetForm = () => {
|
||||
formState.value = cloneDeep(initialFormState);
|
||||
const today = getToday();
|
||||
createTime.value = today;
|
||||
formState.value.faves.createTime = toISOString(today);
|
||||
};
|
||||
|
||||
watch(
|
||||
@@ -94,9 +99,9 @@ watch(
|
||||
(doubanMovie) => {
|
||||
if (doubanMovie) {
|
||||
formState.value = cloneDeep(doubanMovie);
|
||||
createTime.value = toDatetimeLocal(formState.value.faves.createTime);
|
||||
createTime.value = toDateInput(formState.value.faves.createTime) || getToday();
|
||||
}else {
|
||||
createTime.value = undefined;
|
||||
createTime.value = getToday();
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -108,6 +113,9 @@ watch(
|
||||
() => createTime.value,
|
||||
(value) => {
|
||||
formState.value.faves.createTime = value ? toISOString(value) : undefined;
|
||||
},
|
||||
{
|
||||
immediate: true,
|
||||
}
|
||||
);
|
||||
const annotationsFormRef = ref();
|
||||
@@ -223,6 +231,7 @@ const handleSaveFriend = async () => {
|
||||
name="type"
|
||||
label="类型"
|
||||
placeholder="输入类型 value,例如 movie"
|
||||
validation="required"
|
||||
></FormKit>
|
||||
<FormKit
|
||||
v-if="formState.spec.dataType=='halo'"
|
||||
@@ -231,12 +240,10 @@ const handleSaveFriend = async () => {
|
||||
name="cardSubtitle"
|
||||
label="描述"
|
||||
:rows="4"
|
||||
validation="required|length:0,300"
|
||||
validation="required|length:0,1000"
|
||||
></FormKit>
|
||||
<FormKit
|
||||
type="datetime-local"
|
||||
min="0000-01-01T00:00"
|
||||
max="9999-12-31T23:59"
|
||||
type="date"
|
||||
v-model="createTime"
|
||||
name="createTime"
|
||||
validation="required"
|
||||
@@ -252,6 +259,7 @@ const handleSaveFriend = async () => {
|
||||
v-model="formState.faves.status"
|
||||
name="status"
|
||||
type="select"
|
||||
validation="required"
|
||||
></FormKit>
|
||||
<FormKit
|
||||
type="textarea"
|
||||
|
||||
@@ -47,10 +47,16 @@ export function toDatetimeLocal(date: string | Date | undefined | null): string
|
||||
return dayjs(date).format("YYYY-MM-DDTHH:mm");
|
||||
}
|
||||
|
||||
export function toDateInput(date: string | Date | undefined | null): string {
|
||||
if (!date) {
|
||||
return "";
|
||||
}
|
||||
return dayjs(date).format("YYYY-MM-DD");
|
||||
}
|
||||
|
||||
export function toISOString(date: string | Date | undefined | null): string {
|
||||
if (!date) {
|
||||
return "";
|
||||
}
|
||||
return dayjs(date).utc(false).toISOString();
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user