From 0250355dcd20e4162d683fecb3aac5af3a7cd620 Mon Sep 17 00:00:00 2001 From: Blizzard Date: Fri, 17 Jul 2026 11:06:21 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20RSS=20=E8=AE=A2=E9=98=85=E6=BA=90=20+?= =?UTF-8?q?=20=E4=BB=93=E5=BA=93=20README?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GET /rss.xml 输出 RSS 2.0(已发布文章,SUNDYNIX_SITE_URL 可配) - vite 代理补 /rss.xml,页脚 RSS 链接生效 - README:结构、开发流程、环境变量、后端规范、API 速览 Co-Authored-By: Claude Opus 4.8 --- .env.example | 2 + README.md | 58 +++++++++++++++++++++ server/internal/handler/rss.go | 88 ++++++++++++++++++++++++++++++++ server/internal/router/router.go | 3 ++ web/vite.config.ts | 1 + 5 files changed, 152 insertions(+) create mode 100644 README.md create mode 100644 server/internal/handler/rss.go diff --git a/.env.example b/.env.example index 5fb43a4..167128d 100644 --- a/.env.example +++ b/.env.example @@ -7,3 +7,5 @@ SUNDYNIX_ADMIN_USER=admin SUNDYNIX_ADMIN_PASS= SUNDYNIX_JWT_SECRET= SUNDYNIX_NODE_ID=1 +# RSS 等对外链接使用的站点地址 +SUNDYNIX_SITE_URL=https://sundynix.com diff --git a/README.md b/README.md new file mode 100644 index 0000000..3714310 --- /dev/null +++ b/README.md @@ -0,0 +1,58 @@ +# sundynix-site + +[sundynix-agentix](https://git.sundynix.cn/Blizzard/sundynix-agentix)(事件驱动的 AI Agent 工作台)的产品官网 + 内容管理端。 + +## 结构 + +``` +├── web/ 用户端官网 Vite + React + TS + Tailwind(挂 /) +├── admin/ 内容管理端 同栈,JWT 登录(挂 /admin) +├── server/ Go 后端 Gin + GORM,MySQL / SQLite +│ └── internal/webfs/ 两个前端的构建产物 embed 到这里 +└── Makefile 构建入口 +``` + +生产形态:`make build` 把 web、admin 的 dist 打进 Go 二进制,单文件部署整站(`bin/sundynix-site`)。 + +## 本地开发 + +```bash +cp .env.example .env # 填 SUNDYNIX_DB(留空用本地 SQLite) + +make dev-server # 后端 :8090 +make dev-web # 用户端 http://localhost:5173(/api 已代理) +make dev-admin # 管理端 http://localhost:5174/admin/ +``` + +管理端默认账号 `admin / admin123`(务必用环境变量覆盖)。 + +## 环境变量 + +| 变量 | 说明 | 默认 | +|------|------|------| +| `SUNDYNIX_DB` | 数据库 DSN;含 `@tcp(` 走 MySQL,否则视为 SQLite 文件路径 | `sundynix-site.db` | +| `SUNDYNIX_ADDR` | 监听地址 | `:8090` | +| `SUNDYNIX_ADMIN_USER` / `SUNDYNIX_ADMIN_PASS` | 管理端账号 | `admin` / `admin123` | +| `SUNDYNIX_JWT_SECRET` | JWT 密钥 | 开发默认值(生产必配) | +| `SUNDYNIX_NODE_ID` | 雪花算法节点号 | `1` | +| `SUNDYNIX_SITE_URL` | RSS 等对外链接的站点地址 | `https://sundynix.com` | + +支持根目录 `.env`(已 gitignore)。 + +## 后端规范 + +- 表名前缀 `sundynix_`,单数表名(GORM NamingStrategy) +- 主键为**字符串雪花 ID**(`internal/pkg/idgen`,模型嵌入 `BaseModel` 自动生成) +- 列名 snake_case,禁止驼峰 +- 统一响应信封 `{code, message, data}`(`internal/pkg/resp`),code=0 成功 +- 公共分页参数 `page / page_size / keyword`(`internal/pkg/req.PageQuery`) + +## API 速览 + +| 方法 | 路径 | 说明 | +|------|------|------| +| GET | `/api/posts` · `/api/posts/:slug` | 公开文章列表 / 详情 | +| GET | `/rss.xml` | RSS 2.0 订阅源 | +| POST | `/api/admin/login` | 登录,签发 JWT | +| GET/POST | `/api/admin/posts` | 文章分页列表(含草稿)/ 新建 | +| GET/PUT/DELETE | `/api/admin/posts/:id` | 详情 / 更新(含发布切换)/ 删除 | diff --git a/server/internal/handler/rss.go b/server/internal/handler/rss.go new file mode 100644 index 0000000..a63defc --- /dev/null +++ b/server/internal/handler/rss.go @@ -0,0 +1,88 @@ +package handler + +import ( + "encoding/xml" + "net/http" + "os" + "time" + + "github.com/gin-gonic/gin" + "gorm.io/gorm" + + "git.sundynix.cn/Blizzard/sundynix-site/server/internal/model" +) + +// RSSHandler 输出博客的 RSS 2.0 订阅源。 +type RSSHandler struct { + db *gorm.DB + siteURL string +} + +func NewRSSHandler(db *gorm.DB) *RSSHandler { + siteURL := os.Getenv("SUNDYNIX_SITE_URL") + if siteURL == "" { + siteURL = "https://sundynix.com" + } + return &RSSHandler{db: db, siteURL: siteURL} +} + +type rssItem struct { + Title string `xml:"title"` + Link string `xml:"link"` + GUID string `xml:"guid"` + Description string `xml:"description"` + Category string `xml:"category,omitempty"` + PubDate string `xml:"pubDate"` +} + +type rssFeed struct { + XMLName xml.Name `xml:"rss"` + Version string `xml:"version,attr"` + Channel struct { + Title string `xml:"title"` + Link string `xml:"link"` + Description string `xml:"description"` + Language string `xml:"language"` + Items []rssItem `xml:"item"` + } `xml:"channel"` +} + +// Feed GET /rss.xml +func (h *RSSHandler) Feed(c *gin.Context) { + var posts []model.PostListItem + err := h.db.Model(&model.Post{}). + Where("published_at IS NOT NULL"). + Order("published_at DESC"). + Limit(50). + Find(&posts).Error + if err != nil { + c.String(http.StatusInternalServerError, "feed unavailable") + return + } + + feed := rssFeed{Version: "2.0"} + feed.Channel.Title = "sundynix agentix 博客" + feed.Channel.Link = h.siteURL + "/blog" + feed.Channel.Description = "事件驱动的 AI Agent 工作台 — 发布日志与工程笔记" + feed.Channel.Language = "zh-cn" + for _, p := range posts { + link := h.siteURL + "/blog/" + p.Slug + item := rssItem{ + Title: p.Title, + Link: link, + GUID: link, + Description: p.Summary, + Category: p.Category, + } + if p.PublishedAt != nil { + item.PubDate = p.PublishedAt.Format(time.RFC1123Z) + } + feed.Channel.Items = append(feed.Channel.Items, item) + } + + c.Header("Content-Type", "application/rss+xml; charset=utf-8") + c.String(http.StatusOK, xml.Header) + if out, err := xml.MarshalIndent(feed, "", " "); err == nil { + c.Writer.Write(out) + } +} diff --git a/server/internal/router/router.go b/server/internal/router/router.go index 9da290d..78d2009 100644 --- a/server/internal/router/router.go +++ b/server/internal/router/router.go @@ -30,6 +30,9 @@ func New(db *gorm.DB) (*gin.Engine, error) { api.GET("/posts/:slug", posts.Get) } + // RSS 订阅源(页脚 RSS 链接指向这里) + r.GET("/rss.xml", handler.NewRSSHandler(db).Feed) + // ── 管理端 API ── adminAuth := handler.NewAdminAuthHandler() api.POST("/admin/login", adminAuth.Login) diff --git a/web/vite.config.ts b/web/vite.config.ts index c78bbb9..955154d 100644 --- a/web/vite.config.ts +++ b/web/vite.config.ts @@ -16,6 +16,7 @@ export default defineConfig({ proxy: { // 联调 Go 后端时启用:/api → gin '/api': 'http://localhost:8090', + '/rss.xml': 'http://localhost:8090', }, }, })