feat: RSS 订阅源 + 仓库 README
- GET /rss.xml 输出 RSS 2.0(已发布文章,SUNDYNIX_SITE_URL 可配) - vite 代理补 /rss.xml,页脚 RSS 链接生效 - README:结构、开发流程、环境变量、后端规范、API 速览 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -7,3 +7,5 @@ SUNDYNIX_ADMIN_USER=admin
|
|||||||
SUNDYNIX_ADMIN_PASS=
|
SUNDYNIX_ADMIN_PASS=
|
||||||
SUNDYNIX_JWT_SECRET=
|
SUNDYNIX_JWT_SECRET=
|
||||||
SUNDYNIX_NODE_ID=1
|
SUNDYNIX_NODE_ID=1
|
||||||
|
# RSS 等对外链接使用的站点地址
|
||||||
|
SUNDYNIX_SITE_URL=https://sundynix.com
|
||||||
|
|||||||
@@ -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` | 详情 / 更新(含发布切换)/ 删除 |
|
||||||
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -30,6 +30,9 @@ func New(db *gorm.DB) (*gin.Engine, error) {
|
|||||||
api.GET("/posts/:slug", posts.Get)
|
api.GET("/posts/:slug", posts.Get)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// RSS 订阅源(页脚 RSS 链接指向这里)
|
||||||
|
r.GET("/rss.xml", handler.NewRSSHandler(db).Feed)
|
||||||
|
|
||||||
// ── 管理端 API ──
|
// ── 管理端 API ──
|
||||||
adminAuth := handler.NewAdminAuthHandler()
|
adminAuth := handler.NewAdminAuthHandler()
|
||||||
api.POST("/admin/login", adminAuth.Login)
|
api.POST("/admin/login", adminAuth.Login)
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ export default defineConfig({
|
|||||||
proxy: {
|
proxy: {
|
||||||
// 联调 Go 后端时启用:/api → gin
|
// 联调 Go 后端时启用:/api → gin
|
||||||
'/api': 'http://localhost:8090',
|
'/api': 'http://localhost:8090',
|
||||||
|
'/rss.xml': 'http://localhost:8090',
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|||||||
Reference in New Issue
Block a user