From edd12b6488619f775372c30020591ac77fb69180 Mon Sep 17 00:00:00 2001 From: anian Date: Sat, 18 Jul 2026 19:41:46 +0800 Subject: [PATCH] =?UTF-8?q?=E5=B1=8F=E8=94=BD=E7=A9=BA=E5=86=85=E5=AE=B9?= =?UTF-8?q?=EF=BC=9B=E6=8E=A5=E5=85=A5=E6=90=9C=E7=B4=A2=E5=BC=95=E6=93=8E?= =?UTF-8?q?=EF=BC=9B=E6=96=B0=E5=A2=9E=E8=AF=A6=E6=83=85=E9=A1=B5=E9=9D=A2?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- README.md | 5 +- gradle.properties | 2 +- pnpm-lock.yaml | 3 + .../friends/reconciler/RssSyncReconciler.java | 81 ++++++----- .../la/moony/friends/rest/FriendRouter.java | 13 +- .../search/FriendPostDocumentConverter.java | 101 +++++++++++++ .../FriendPostHaloDocumentsProvider.java | 59 ++++++++ .../search/FriendPostSearchReconciler.java | 61 ++++++++ src/main/resources/templates/friend-post.html | 133 ++++++++++++++++++ src/main/resources/templates/friends.html | 7 +- 10 files changed, 422 insertions(+), 43 deletions(-) create mode 100644 src/main/java/la/moony/friends/search/FriendPostDocumentConverter.java create mode 100644 src/main/java/la/moony/friends/search/FriendPostHaloDocumentsProvider.java create mode 100644 src/main/java/la/moony/friends/search/FriendPostSearchReconciler.java create mode 100644 src/main/resources/templates/friend-post.html diff --git a/README.md b/README.md index 983ad57..1c03526 100644 --- a/README.md +++ b/README.md @@ -2,8 +2,11 @@ > 基于原作者v1.4.3代码修改 -#### v1.4.3-4-补丁 +#### v1.4.3-5 1. 补充ui/package.json内"@rsbuild/core": "^1.4.3"依赖 +2. 新增单条朋友圈页面(前端模板friend-post.html;访问地址/friends/friend-post-xxxxx) +3. 朋友圈内容可搜索并返回单条朋友圈页面链接 +4. 内容(description)为空的项目将不再存入数据库 #### v1.4.3-4 1. 控制台新增并行处理数量、图片下载开关、图片下载位置、图片下载代理等配置 diff --git a/gradle.properties b/gradle.properties index 8f8d2a6..d125da4 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1 +1 @@ -version=1.4.3-4 +version=1.4.3-5 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index dc419c8..6096d4d 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -126,6 +126,9 @@ importers: '@iconify/json': specifier: ^2.2.224 version: 2.2.446 + '@rsbuild/core': + specifier: ^1.4.3 + version: 1.7.3 '@rspack/cli': specifier: 1.4.0-beta.0 version: 1.4.0-beta.0(@rspack/core@1.4.0-beta.0(@swc/helpers@0.5.19))(@types/express@4.17.25)(tslib@2.8.1)(webpack@5.105.4) diff --git a/src/main/java/la/moony/friends/reconciler/RssSyncReconciler.java b/src/main/java/la/moony/friends/reconciler/RssSyncReconciler.java index f594391..a0ea76f 100644 --- a/src/main/java/la/moony/friends/reconciler/RssSyncReconciler.java +++ b/src/main/java/la/moony/friends/reconciler/RssSyncReconciler.java @@ -242,44 +242,40 @@ public class RssSyncReconciler implements Reconciler, friendPostSpec.setAuthorUrl(link.getSpec().getUrl()); friendPostSpec.setLinkName(linkName); String description = friendPost.getSpec().getDescription(); - - // 先提取图片和视频 poster 文件名覆盖标题,再将 HTML 描述转换为纯文本 - if (StringUtils.isNotEmpty(description)) { - List imageFileNames = new ArrayList<>(); - Matcher matcher = IMG_SRC_PATTERN.matcher(description); - while (matcher.find()) { - String imgSrc = matcher.group(2); - Matcher mw2000Matcher = MW2000_IMAGE_FILE_PATTERN.matcher(imgSrc); - if (mw2000Matcher.matches()) { - imageFileNames.add(mw2000Matcher.group(1)); - } - } - - Matcher posterMatcher = VIDEO_POSTER_PATTERN.matcher(description); - while (posterMatcher.find()) { - String posterSrc = posterMatcher.group(2); - String posterFileName = extractFileNameFromUrl(posterSrc); - if (StringUtils.isNotBlank(posterFileName)) { - imageFileNames.add(posterFileName); - } - } - - if (enableWeiboImageRecordAndDownload && !imageFileNames.isEmpty()) { - String imageFiles = String.join(",", imageFileNames); - friendPostSpec.setImageFiles(imageFiles); - // 通知图片处理服务 - notifyImageService(imageFiles, imageSavePath, imageBaseUrl, - imageReferer); - } - - String pureText = CommonUtils.parseAndTruncateHtml2Text(description, 100000); - String regexp = "[ *|\\s*]*"; - pureText = pureText.replaceFirst(regexp, "").trim(); - String weiboVideoRegex = "\\s[^\\s]*的微博视频 视频无法显示,请前往微博视频观看。"; - pureText = pureText.replaceAll(weiboVideoRegex, ""); - friendPostSpec.setDescription(pureText); + String pureText = normalizeDescription(description); + if (StringUtils.isBlank(pureText)) { + continue; } + // 提取图片和视频 poster 文件名,并保存过滤后的纯文本描述 + List imageFileNames = new ArrayList<>(); + Matcher matcher = IMG_SRC_PATTERN.matcher(description); + while (matcher.find()) { + String imgSrc = matcher.group(2); + Matcher mw2000Matcher = MW2000_IMAGE_FILE_PATTERN.matcher(imgSrc); + if (mw2000Matcher.matches()) { + imageFileNames.add(mw2000Matcher.group(1)); + } + } + + Matcher posterMatcher = VIDEO_POSTER_PATTERN.matcher(description); + while (posterMatcher.find()) { + String posterSrc = posterMatcher.group(2); + String posterFileName = extractFileNameFromUrl(posterSrc); + if (StringUtils.isNotBlank(posterFileName)) { + imageFileNames.add(posterFileName); + } + } + + if (enableWeiboImageRecordAndDownload && !imageFileNames.isEmpty()) { + String imageFiles = String.join(",", imageFileNames); + friendPostSpec.setImageFiles(imageFiles); + // 通知图片处理服务 + notifyImageService(imageFiles, imageSavePath, imageBaseUrl, + imageReferer); + } + + friendPostSpec.setDescription(pureText); client.create(friendPost); } } @@ -287,6 +283,17 @@ public class RssSyncReconciler implements Reconciler, } } + private String normalizeDescription(String description) { + if (StringUtils.isEmpty(description)) { + return ""; + } + String pureText = CommonUtils.parseAndTruncateHtml2Text(description, 100000); + String regexp = "[ *|\\s*]*"; + pureText = pureText.replaceFirst(regexp, "").trim(); + String weiboVideoRegex = "\\s[^\\s]*的微博视频 视频无法显示,请前往微博视频观看。"; + return pureText.replaceAll(weiboVideoRegex, "").trim(); + } + public List fetchFriendPost(String rssAddress, int postsLimit, RssFeedSyncLog sync) { List friendPostList = new ArrayList<>(); try { @@ -498,4 +505,4 @@ public class RssSyncReconciler implements Reconciler, String imageSavePath, String imageBaseUrl, String imageReferer, int parallelCount) { } -} \ No newline at end of file +} diff --git a/src/main/java/la/moony/friends/rest/FriendRouter.java b/src/main/java/la/moony/friends/rest/FriendRouter.java index 20eeaf6..7ed42f0 100644 --- a/src/main/java/la/moony/friends/rest/FriendRouter.java +++ b/src/main/java/la/moony/friends/rest/FriendRouter.java @@ -40,6 +40,7 @@ public class FriendRouter { RouterFunction friendTemplateRoute() { return RouterFunctions.route().GET("/friends",this::handlerFunction) .GET("/friends/page/{page:\\d+}",this::handlerFunction) + .GET("/friends/{friendPostName:\\S+}", this::friendPostDetail) .build(); } @@ -54,6 +55,16 @@ public class FriendRouter { )); } + private Mono friendPostDetail(ServerRequest request) { + String friendPostName = request.pathVariable("friendPostName"); + return templateNameResolver.resolveTemplateNameOrDefault(request.exchange(), "friend-post") + .flatMap(templateName -> ServerResponse.ok().render(templateName, + Map.of(ModelConst.TEMPLATE_ID, templateName, + "friend", friendFinder.getByName(friendPostName), + "title", getFriendTitle()) + )); + } + Mono getFriendTitle() { return this.settingFetcher.get("base").map( setting -> setting.get("title").asText("友链朋友圈")).defaultIfEmpty( @@ -134,4 +145,4 @@ public class FriendRouter { return NumberUtils.toInt(page, 1); } -} \ No newline at end of file +} diff --git a/src/main/java/la/moony/friends/search/FriendPostDocumentConverter.java b/src/main/java/la/moony/friends/search/FriendPostDocumentConverter.java new file mode 100644 index 0000000..ebbf12f --- /dev/null +++ b/src/main/java/la/moony/friends/search/FriendPostDocumentConverter.java @@ -0,0 +1,101 @@ +package la.moony.friends.search; + +import static la.moony.friends.search.FriendPostHaloDocumentsProvider.FRIEND_POST_DOCUMENT_TYPE; + +import java.time.Instant; +import java.util.List; +import java.util.Optional; +import la.moony.friends.extension.FriendPost; +import la.moony.friends.util.CommonUtils; +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 FriendPostDocumentConverter implements Converter> { + + private static final String EMPTY_CONTENT = "无内容"; + + private final ExternalUrlSupplier externalUrlSupplier; + + @Override + @NonNull + public Mono convert(FriendPost friendPost) { + var spec = friendPost.getSpec(); + var author = authorName(friendPost); + var title = defaultIfBlank(spec == null ? null : spec.getTitle(), EMPTY_CONTENT); + var description = defaultIfBlank(plainDescription(spec), EMPTY_CONTENT); + var haloDoc = new HaloDocument(); + haloDoc.setMetadataName(friendPost.getMetadata().getName()); + haloDoc.setType(FRIEND_POST_DOCUMENT_TYPE); + haloDoc.setId(haloDocId(friendPost)); + haloDoc.setTitle(title); + haloDoc.setDescription(description); + haloDoc.setContent(content(title, author, description)); + haloDoc.setExposed(true); + haloDoc.setTags(tags(author)); + haloDoc.setOwnerName(author); + haloDoc.setUpdateTimestamp(updateTimestamp(friendPost)); + haloDoc.setCreationTimestamp(creationTimestamp(friendPost)); + haloDoc.setPermalink(permalink(friendPost)); + haloDoc.setPublished(true); + return Mono.just(haloDoc); + } + + String haloDocId(FriendPost friendPost) { + return FRIEND_POST_DOCUMENT_TYPE + '-' + friendPost.getMetadata().getName(); + } + + private String authorName(FriendPost friendPost) { + var author = Optional.ofNullable(friendPost.getSpec()) + .map(FriendPost.FriendPostSpec::getAuthor) + .orElse(null); + return StringUtils.defaultIfBlank(author, EMPTY_CONTENT); + } + + private String plainDescription(FriendPost.FriendPostSpec spec) { + if (spec == null) { + return null; + } + return CommonUtils.parseAndTruncateHtml2Text(defaultString(spec.getDescription()), 300); + } + + private String content(String title, String author, String description) { + return String.join(" ", title, author, description).trim(); + } + + private List tags(String author) { + return StringUtils.isBlank(author) ? List.of() : List.of(author); + } + + private Instant updateTimestamp(FriendPost friendPost) { + var spec = friendPost.getSpec(); + return Optional.ofNullable(spec) + .map(FriendPost.FriendPostSpec::getPubDate) + .orElse(creationTimestamp(friendPost)); + } + + private Instant creationTimestamp(FriendPost friendPost) { + return Optional.ofNullable(friendPost.getMetadata().getCreationTimestamp()) + .orElse(Instant.EPOCH); + } + + private String permalink(FriendPost friendPost) { + var externalUrl = externalUrlSupplier.get(); + return externalUrl.resolve("friends/" + friendPost.getMetadata().getName()).toString(); + } + + private static String defaultString(String value) { + return StringUtils.defaultString(value); + } + + private static String defaultIfBlank(String value, String defaultValue) { + return StringUtils.defaultIfBlank(value, defaultValue); + } +} diff --git a/src/main/java/la/moony/friends/search/FriendPostHaloDocumentsProvider.java b/src/main/java/la/moony/friends/search/FriendPostHaloDocumentsProvider.java new file mode 100644 index 0000000..8ac2ede --- /dev/null +++ b/src/main/java/la/moony/friends/search/FriendPostHaloDocumentsProvider.java @@ -0,0 +1,59 @@ +package la.moony.friends.search; + +import la.moony.friends.extension.FriendPost; +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 FriendPostHaloDocumentsProvider implements HaloDocumentsProvider { + + public static final String FRIEND_POST_DOCUMENT_TYPE = "friendpost.friend.moony.la"; + + private static final int PAGE_SIZE = 200; + + private final ReactiveExtensionClient client; + + private final FriendPostDocumentConverter converter; + + @Override + public Flux fetchAll() { + var options = new ListOptions(); + var notDeleted = isNull("metadata.deletionTimestamp"); + options.setFieldSelector(FieldSelector.of(notDeleted)); + var pageRequest = createPageRequest(); + return client.listBy(FriendPost.class, options, pageRequest) + .expand(result -> result.hasNext() + ? client.listBy(FriendPost.class, options, nextPage(result, pageRequest.getSort())) + : Mono.empty()) + .flatMap(result -> Flux.fromStream(result.get())) + .flatMap(converter::convert); + } + + @Override + public String getType() { + return FRIEND_POST_DOCUMENT_TYPE; + } + + private PageRequest createPageRequest() { + return PageRequestImpl.of(1, PAGE_SIZE, + Sort.by("metadata.creationTimestamp", "metadata.name")); + } + + private static PageRequest nextPage(ListResult result, Sort sort) { + return PageRequestImpl.of(result.getPage() + 1, result.getSize(), sort); + } +} diff --git a/src/main/java/la/moony/friends/search/FriendPostSearchReconciler.java b/src/main/java/la/moony/friends/search/FriendPostSearchReconciler.java new file mode 100644 index 0000000..ad29d91 --- /dev/null +++ b/src/main/java/la/moony/friends/search/FriendPostSearchReconciler.java @@ -0,0 +1,61 @@ +package la.moony.friends.search; + +import java.util.List; +import java.util.Set; +import la.moony.friends.extension.FriendPost; +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 FriendPostSearchReconciler implements Reconciler { + + private static final String FINALIZER = "friend-post-search-protection"; + + private final ApplicationEventPublisher eventPublisher; + + private final ExtensionClient client; + + private final FriendPostDocumentConverter converter; + + @Override + public Result reconcile(Request request) { + client.fetch(FriendPost.class, request.name()).ifPresent(friendPost -> { + if (ExtensionUtil.isDeleted(friendPost)) { + if (ExtensionUtil.removeFinalizers(friendPost.getMetadata(), Set.of(FINALIZER))) { + eventPublisher.publishEvent( + new HaloDocumentDeleteRequestEvent(this, + List.of(converter.haloDocId(friendPost))) + ); + client.update(friendPost); + } + return; + } + ExtensionUtil.addFinalizers(friendPost.getMetadata(), Set.of(FINALIZER)); + + var haloDoc = converter.convert(friendPost) + .blockOptional().orElseThrow(); + eventPublisher.publishEvent( + new HaloDocumentAddRequestEvent(this, List.of(haloDoc))); + + client.update(friendPost); + }); + return Result.doNotRetry(); + } + + @Override + public Controller setupWith(ControllerBuilder builder) { + return builder + .extension(new FriendPost()) + .workerCount(1) + .build(); + } +} diff --git a/src/main/resources/templates/friend-post.html b/src/main/resources/templates/friend-post.html new file mode 100644 index 0000000..76fcb8e --- /dev/null +++ b/src/main/resources/templates/friend-post.html @@ -0,0 +1,133 @@ + + + + + + + + + + + + +
+ ©2024 [[${site.title}]] +
+ + diff --git a/src/main/resources/templates/friends.html b/src/main/resources/templates/friends.html index cdd805c..26426a7 100644 --- a/src/main/resources/templates/friends.html +++ b/src/main/resources/templates/friends.html @@ -1511,9 +1511,10 @@
-

+

-

+

@@ -1556,4 +1557,4 @@ - \ No newline at end of file +