From 1a8e81bb5b68c748600467accb7ccac83ce60b58 Mon Sep 17 00:00:00 2001 From: anian Date: Sun, 26 Jul 2026 06:54:23 +0800 Subject: [PATCH] =?UTF-8?q?=E6=8E=A5=E5=85=A5=E6=90=9C=E7=B4=A2=E5=BC=95?= =?UTF-8?q?=E6=93=8E=EF=BC=9B=20=E4=BF=AE=E6=94=B9=E6=8E=A7=E5=88=B6?= =?UTF-8?q?=E5=8F=B0=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 4 ++ gradle.properties | 2 +- .../java/la/moony/douban/DoubanPlugin.java | 4 ++ .../java/la/moony/douban/DoubanRouter.java | 9 +++ .../search/DoubanMovieDocumentConverter.java | 59 +++++++++++++++++++ .../DoubanMovieHaloDocumentsProvider.java | 49 +++++++++++++++ .../search/DoubanMovieSearchReconciler.java | 49 +++++++++++++++ src/main/resources/static/db.js | 10 ++-- .../resources/templates/douban-detail.html | 25 ++++++++ src/main/resources/templates/douban.html | 4 +- ui/src/components/DoubanMovieEditingModal.vue | 22 ++++--- ui/src/utils/date.ts | 8 ++- 12 files changed, 229 insertions(+), 16 deletions(-) create mode 100644 src/main/java/la/moony/douban/search/DoubanMovieDocumentConverter.java create mode 100644 src/main/java/la/moony/douban/search/DoubanMovieHaloDocumentsProvider.java create mode 100644 src/main/java/la/moony/douban/search/DoubanMovieSearchReconciler.java create mode 100644 src/main/resources/templates/douban-detail.html diff --git a/README.md b/README.md index 55d6565..1a6b815 100644 --- a/README.md +++ b/README.md @@ -2,6 +2,10 @@ > 基于原作者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代码) diff --git a/gradle.properties b/gradle.properties index 5f874f4..ef3a2ed 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=1.2.2-2 +version=1.2.2-3 diff --git a/src/main/java/la/moony/douban/DoubanPlugin.java b/src/main/java/la/moony/douban/DoubanPlugin.java index 59a40a4..ef5fdc2 100644 --- a/src/main/java/la/moony/douban/DoubanPlugin.java +++ b/src/main/java/la/moony/douban/DoubanPlugin.java @@ -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( diff --git a/src/main/java/la/moony/douban/DoubanRouter.java b/src/main/java/la/moony/douban/DoubanRouter.java index f6a8dc4..81f2ae3 100644 --- a/src/main/java/la/moony/douban/DoubanRouter.java +++ b/src/main/java/la/moony/douban/DoubanRouter.java @@ -29,9 +29,18 @@ public class DoubanRouter { RouterFunction friendTemplateRoute() { return RouterFunctions.route().GET("/books",this::handlerFunction) + .GET("/books/{doubanMovieName:\\S+}", this::doubanMovieDetail) .build(); } + private Mono 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 handlerFunction(ServerRequest request) { String type = request.queryParam("type") .filter(StringUtils::isNotBlank) diff --git a/src/main/java/la/moony/douban/search/DoubanMovieDocumentConverter.java b/src/main/java/la/moony/douban/search/DoubanMovieDocumentConverter.java new file mode 100644 index 0000000..0f7219b --- /dev/null +++ b/src/main/java/la/moony/douban/search/DoubanMovieDocumentConverter.java @@ -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> { + + private final ExternalUrlSupplier externalUrlSupplier; + + @Override + @NonNull + public Mono 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(); + } +} diff --git a/src/main/java/la/moony/douban/search/DoubanMovieHaloDocumentsProvider.java b/src/main/java/la/moony/douban/search/DoubanMovieHaloDocumentsProvider.java new file mode 100644 index 0000000..805e481 --- /dev/null +++ b/src/main/java/la/moony/douban/search/DoubanMovieHaloDocumentsProvider.java @@ -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 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; + } +} diff --git a/src/main/java/la/moony/douban/search/DoubanMovieSearchReconciler.java b/src/main/java/la/moony/douban/search/DoubanMovieSearchReconciler.java new file mode 100644 index 0000000..5aa998f --- /dev/null +++ b/src/main/java/la/moony/douban/search/DoubanMovieSearchReconciler.java @@ -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 { + + 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(); + } +} diff --git a/src/main/resources/static/db.js b/src/main/resources/static/db.js index 7872a6a..dbf483f 100644 --- a/src/main/resources/static/db.js +++ b/src/main/resources/static/db.js @@ -182,8 +182,8 @@ class HALO_DOUBAN { }${ movie.spec.year > 0 ? " · " + movie.spec.year : "" } + `/books/${movie.metadata.name}` + }">${movie.spec.name} `; }) @@ -212,8 +212,8 @@ class HALO_DOUBAN { }${ item.spec.year > 0 ? " · " + item.spec.year : "" } + `/books/${item.metadata.name}` + }">${item.spec.name} `; }) @@ -294,4 +294,4 @@ class HALO_DOUBAN { } } -new HALO_DOUBAN(); \ No newline at end of file +new HALO_DOUBAN(); diff --git a/src/main/resources/templates/douban-detail.html b/src/main/resources/templates/douban-detail.html new file mode 100644 index 0000000..4a5cab4 --- /dev/null +++ b/src/main/resources/templates/douban-detail.html @@ -0,0 +1,25 @@ + + + + + + 豆瓣记录 + + + +
+
+
+
+
+
标题
+
描述
+

备注

+ 查看豆瓣原页 +
+
+
+

未找到该条豆瓣记录。

+
+ + diff --git a/src/main/resources/templates/douban.html b/src/main/resources/templates/douban.html index f4663a1..0c5a52d 100644 --- a/src/main/resources/templates/douban.html +++ b/src/main/resources/templates/douban.html @@ -83,9 +83,9 @@
- +
- \ No newline at end of file + diff --git a/ui/src/components/DoubanMovieEditingModal.vue b/ui/src/components/DoubanMovieEditingModal.vue index 61362b2..1b44e22 100644 --- a/ui/src/components/DoubanMovieEditingModal.vue +++ b/ui/src/components/DoubanMovieEditingModal.vue @@ -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(false); const formVisible = ref(false); const createTime = ref(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" > { name="cardSubtitle" label="描述" :rows="4" - validation="required|length:0,300" + validation="required|length:0,1000" > { v-model="formState.faves.status" name="status" type="select" + validation="required" >