diff --git a/backend/src/main/java/com/openisle/config/SecurityConfig.java b/backend/src/main/java/com/openisle/config/SecurityConfig.java
index 100c0a2e3..9fabbacbd 100644
--- a/backend/src/main/java/com/openisle/config/SecurityConfig.java
+++ b/backend/src/main/java/com/openisle/config/SecurityConfig.java
@@ -119,6 +119,7 @@ public class SecurityConfig {
.requestMatchers(HttpMethod.GET, "/api/reaction-types").permitAll()
.requestMatchers(HttpMethod.GET, "/api/activities/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/sitemap.xml").permitAll()
+ .requestMatchers(HttpMethod.GET, "/api/rss").permitAll()
.requestMatchers(HttpMethod.GET, "/api/point-goods").permitAll()
.requestMatchers(HttpMethod.POST, "/api/point-goods").permitAll()
.requestMatchers(HttpMethod.POST, "/api/categories/**").hasAuthority("ADMIN")
@@ -154,7 +155,8 @@ public class SecurityConfig {
uri.startsWith("/api/reaction-types") || uri.startsWith("/api/config") ||
uri.startsWith("/api/activities") || uri.startsWith("/api/push/public-key") ||
uri.startsWith("/api/point-goods") ||
- uri.startsWith("/api/sitemap.xml") || uri.startsWith("/api/medals"));
+ uri.startsWith("/api/sitemap.xml") || uri.startsWith("/api/medals") ||
+ uri.startsWith("/api/rss"));
if (authHeader != null && authHeader.startsWith("Bearer ")) {
String token = authHeader.substring(7);
diff --git a/backend/src/main/java/com/openisle/controller/AdminPostController.java b/backend/src/main/java/com/openisle/controller/AdminPostController.java
index 8e17d1c3e..3c9414cad 100644
--- a/backend/src/main/java/com/openisle/controller/AdminPostController.java
+++ b/backend/src/main/java/com/openisle/controller/AdminPostController.java
@@ -45,4 +45,14 @@ public class AdminPostController {
public PostSummaryDto unpin(@PathVariable Long id) {
return postMapper.toSummaryDto(postService.unpinPost(id));
}
+
+ @PostMapping("/{id}/rss-exclude")
+ public PostSummaryDto excludeFromRss(@PathVariable Long id) {
+ return postMapper.toSummaryDto(postService.excludeFromRss(id));
+ }
+
+ @PostMapping("/{id}/rss-include")
+ public PostSummaryDto includeInRss(@PathVariable Long id) {
+ return postMapper.toSummaryDto(postService.includeInRss(id));
+ }
}
diff --git a/backend/src/main/java/com/openisle/controller/RssController.java b/backend/src/main/java/com/openisle/controller/RssController.java
new file mode 100644
index 000000000..f0a3b98d2
--- /dev/null
+++ b/backend/src/main/java/com/openisle/controller/RssController.java
@@ -0,0 +1,78 @@
+package com.openisle.controller;
+
+import com.openisle.model.Post;
+import com.openisle.service.PostService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Value;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.util.List;
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+@RestController
+@RequiredArgsConstructor
+public class RssController {
+ private final PostService postService;
+
+ @Value("${app.website-url:https://www.open-isle.com}")
+ private String websiteUrl;
+
+ private static final Pattern MD_IMAGE = Pattern.compile("!\\[[^\\]]*\\]\\(([^)]+)\\)");
+ private static final Pattern HTML_IMAGE = Pattern.compile("
]+src=[\"']?([^\"'>]+)[\"']?[^>]*>");
+
+ @GetMapping(value = "/api/rss", produces = "application/rss+xml;charset=UTF-8")
+ public String feed() {
+ List posts = postService.listLatestRssPosts(10);
+ StringBuilder sb = new StringBuilder();
+ sb.append("");
+ sb.append("");
+ sb.append("");
+ sb.append("OpenIsle RSS");
+ sb.append("").append(websiteUrl).append("");
+ sb.append("Latest posts");
+ DateTimeFormatter fmt = DateTimeFormatter.RFC_1123_DATE_TIME;
+ for (Post p : posts) {
+ String link = websiteUrl + "/posts/" + p.getId();
+ sb.append("- ");
+ sb.append("");
+ sb.append("").append(link).append("");
+ sb.append("").append(link).append("");
+ sb.append("")
+ .append(p.getCreatedAt().atZone(ZoneId.systemDefault()).format(fmt))
+ .append("");
+ String desc = p.getContent() + "\n
更多细节请访问原文:" + link + "
";
+ sb.append("");
+ String img = firstImage(p.getContent());
+ if (img != null) {
+ sb.append("");
+ }
+ sb.append(" ");
+ }
+ sb.append("");
+ return sb.toString();
+ }
+
+ private String firstImage(String content) {
+ Matcher m = MD_IMAGE.matcher(content);
+ if (m.find()) {
+ return m.group(1);
+ }
+ m = HTML_IMAGE.matcher(content);
+ if (m.find()) {
+ return m.group(1);
+ }
+ return null;
+ }
+
+ private String getMimeType(String url) {
+ String lower = url.toLowerCase();
+ if (lower.endsWith(".png")) return "image/png";
+ if (lower.endsWith(".gif")) return "image/gif";
+ return "image/jpeg";
+ }
+}
diff --git a/backend/src/main/java/com/openisle/dto/PostSummaryDto.java b/backend/src/main/java/com/openisle/dto/PostSummaryDto.java
index d849c225b..e47a03e11 100644
--- a/backend/src/main/java/com/openisle/dto/PostSummaryDto.java
+++ b/backend/src/main/java/com/openisle/dto/PostSummaryDto.java
@@ -31,5 +31,6 @@ public class PostSummaryDto {
private int pointReward;
private PostType type;
private LotteryDto lottery;
+ private boolean rssExcluded;
}
diff --git a/backend/src/main/java/com/openisle/mapper/PostMapper.java b/backend/src/main/java/com/openisle/mapper/PostMapper.java
index 58652f17b..a80da01f7 100644
--- a/backend/src/main/java/com/openisle/mapper/PostMapper.java
+++ b/backend/src/main/java/com/openisle/mapper/PostMapper.java
@@ -63,6 +63,7 @@ public class PostMapper {
dto.setCommentCount(commentService.countComments(post.getId()));
dto.setStatus(post.getStatus());
dto.setPinnedAt(post.getPinnedAt());
+ dto.setRssExcluded(post.isRssExcluded());
List reactions = reactionService.getReactionsForPost(post.getId())
.stream()
diff --git a/backend/src/main/java/com/openisle/model/Post.java b/backend/src/main/java/com/openisle/model/Post.java
index 0f5a709f1..cbbc75a0e 100644
--- a/backend/src/main/java/com/openisle/model/Post.java
+++ b/backend/src/main/java/com/openisle/model/Post.java
@@ -67,4 +67,7 @@ public class Post {
@Column
private LocalDateTime pinnedAt;
+ @Column(nullable = false)
+ private boolean rssExcluded = false;
+
}
diff --git a/backend/src/main/java/com/openisle/repository/PostRepository.java b/backend/src/main/java/com/openisle/repository/PostRepository.java
index d764bb1dd..58083b193 100644
--- a/backend/src/main/java/com/openisle/repository/PostRepository.java
+++ b/backend/src/main/java/com/openisle/repository/PostRepository.java
@@ -106,4 +106,6 @@ public interface PostRepository extends JpaRepository {
"WHERE p.createdAt >= :start AND p.createdAt < :end GROUP BY d ORDER BY d")
java.util.List