Files
go_blog/handlers/rss.go
T
kevin f307781f58 docs: 全部 Go 代码注释汉化
- 48 个 Go 文件所有注释(行注释/块注释/行尾注释,含 _test.go)翻译为中文
- 保留技术标识符:SECURITY_TODO(n)、unsafe-inline、sqlite/mysql、路由参数等
- 代码、字符串字面量、日志消息保持英文原文,零逻辑改动
- go build/vet 通过,go test -count=1 ./... 全绿
2026-08-27 19:03:03 +08:00

192 lines
5.0 KiB
Go

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
}