package handlers import ( "encoding/xml" "fmt" "html" "log" "net/http" "strings" "time" "github.com/gin-gonic/gin" "go_blog/models" "gorm.io/gorm" ) // RSS 2.0 XML 结构定义。 // RSS 是 RSS 2.0 feed 的根元素。 type RSS struct { XMLName xml.Name `xml:"rss"` Version string `xml:"version,attr"` Channel *Channel `xml:"channel"` } // Channel 表示包含 feed 元数据和条目(items)的 RSS channel。 type Channel struct { Title string `xml:"title"` Link string `xml:"link"` Description string `xml:"description"` Language string `xml:"language"` LastBuildDate string `xml:"lastBuildDate,omitempty"` Items []Item `xml:"item"` } // Item 表示 RSS feed 中的单篇文章。 type Item struct { Title string `xml:"title"` Link string `xml:"link"` Description string `xml:"description"` Author string `xml:"author,omitempty"` PubDate string `xml:"pubDate"` GUID string `xml:"guid"` } // RSSFeed 生成最新已发布文章的 RSS 2.0 feed。 func RSSFeed(db *gorm.DB) gin.HandlerFunc { return func(c *gin.Context) { // 确定用于站点元数据的当前语言。 lang, exists := c.Get("lang") if !exists { lang = "en" } langStr := lang.(string) // 获取站点设置用于 feed 元数据。 siteSetting := &models.SiteSetting{} db.First(siteSetting) // SECURITY_TODO #16:配置后使用设置中的规范化站点 URL—— // 请求的 Host 可被攻击者控制,否则会污染 feed 中的每个链接。 // 对于旧部署则回退并给出警告。 var baseURL string if u := strings.TrimSpace(siteSetting.SiteURL); u != "" { baseURL = strings.TrimRight(u, "/") } else { log.Printf("WARNING: Site URL is not set in settings; RSS links use request Host %q (set settings_site_url to a fixed URL)", c.Request.Host) scheme := "http" if c.Request.TLS != nil || c.GetHeader("X-Forwarded-Proto") == "https" { scheme = "https" } baseURL = fmt.Sprintf("%s://%s", scheme, c.Request.Host) } // 获取最新 20 篇已发布文章。 var articles []models.Article db.Where("status = ?", models.ArticlePublished). Preload("Author"). Order(publishedArticleOrder). Limit(20). Find(&articles) // 构建 channel 元数据。 channel := &Channel{ Title: siteSetting.LogoText(langStr), Link: baseURL, Description: siteSetting.HomeSubtitle(langStr), Language: getRSSLanguage(langStr), Items: make([]Item, 0, len(articles)), } // 将 lastBuildDate 设置为最新文章的发布日期。 if len(articles) > 0 && articles[0].PublishedAt != nil { channel.LastBuildDate = formatRSSTime(*articles[0].PublishedAt) } // 将文章转换为 RSS 条目。 for _, article := range articles { item := Item{ Title: article.Title, Link: fmt.Sprintf("%s/article/%s", baseURL, article.Slug), Description: getArticleDescription(&article), PubDate: formatRSSTime(getArticlePubDate(&article)), GUID: fmt.Sprintf("%s/article/%s", baseURL, article.Slug), } // 添加作者信息。 if article.Author.DisplayName != "" { item.Author = article.Author.DisplayName } else { item.Author = article.Author.Username } channel.Items = append(channel.Items, item) } // 构建 RSS feed。 feed := &RSS{ Version: "2.0", Channel: channel, } // 设置正确的内容类型并返回 XML。 c.Header("Content-Type", "application/rss+xml; charset=utf-8") c.XML(http.StatusOK, feed) } } // getRSSLanguage 将内部语言代码转换为 RSS 语言格式。 func getRSSLanguage(lang string) string { switch lang { case "zh": return "zh-CN" case "en": return "en-US" default: return "en-US" } } // formatRSSTime 将 time.Time 格式化为 RSS 2.0 要求的 RFC1123Z 格式。 func formatRSSTime(t time.Time) string { return t.Format(time.RFC1123Z) } // getArticlePubDate 返回文章的发布日期,无则回退到创建日期。 func getArticlePubDate(article *models.Article) time.Time { if article.PublishedAt != nil { return *article.PublishedAt } return article.CreatedAt } // getArticleDescription 返回文章用于 RSS 的描述。 // 优先使用摘要字段;没有则回退到截断的正文。 func getArticleDescription(article *models.Article) string { if article.Summary != "" { return html.EscapeString(article.Summary) } // 去除 HTML 标签并将正文截断到 200 个字符。 content := stripHTMLTags(article.Content) if len(content) > 200 { content = content[:200] + "..." } return html.EscapeString(content) } // stripHTMLTags 从字符串中去除 HTML 标签(基础实现)。 func stripHTMLTags(s string) string { // 通过查找 < 与 > 的配对去除 HTML 标签。 var result strings.Builder inTag := false for _, r := range s { if r == '<' { inTag = true continue } if r == '>' { inTag = false continue } if !inTag { result.WriteRune(r) } } // 清理多余空格并去除首尾空白。 cleaned := strings.Join(strings.Fields(result.String()), " ") return cleaned }