5adb3d621b
- 后端 Visit 模型(date+ip 唯一索引):POST /api/track 埋点(ClientIP upsert,pv+1),GET /api/admin/visits 每日 UV/PV 统计 - web 每会话上报一次访问(sessionStorage 防重),IP 由后端去重 - admin 访问统计页:侧边栏入口 + 今日/近N天 UV·PV 卡片 + 每日柱状趋势(自绘无依赖) + 7/30/90 天切换 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
96 lines
2.6 KiB
Go
96 lines
2.6 KiB
Go
package router
|
|
|
|
import (
|
|
"io/fs"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"gorm.io/gorm"
|
|
|
|
"git.sundynix.cn/Blizzard/sundynix-site/server/internal/handler"
|
|
"git.sundynix.cn/Blizzard/sundynix-site/server/internal/middleware"
|
|
"git.sundynix.cn/Blizzard/sundynix-site/server/internal/pkg/resp"
|
|
"git.sundynix.cn/Blizzard/sundynix-site/server/internal/webfs"
|
|
)
|
|
|
|
func New(db *gorm.DB) (*gin.Engine, error) {
|
|
r := gin.New()
|
|
r.Use(gin.Logger(), gin.Recovery(), middleware.CORS())
|
|
|
|
// ── 公开 API ──
|
|
api := r.Group("/api")
|
|
{
|
|
api.GET("/healthz", func(c *gin.Context) {
|
|
resp.OK(c, gin.H{"status": "ok"})
|
|
})
|
|
|
|
posts := handler.NewPostHandler(db)
|
|
api.GET("/posts", posts.List)
|
|
api.GET("/posts/:slug", posts.Get)
|
|
|
|
api.GET("/releases/latest", handler.NewReleaseHandler().Latest)
|
|
|
|
api.POST("/track", handler.NewVisitHandler(db).Track)
|
|
}
|
|
|
|
// RSS 订阅源(页脚 RSS 链接指向这里)
|
|
r.GET("/rss.xml", handler.NewRSSHandler(db).Feed)
|
|
|
|
// ── 管理端 API ──
|
|
adminAuth := handler.NewAdminAuthHandler()
|
|
api.POST("/admin/login", adminAuth.Login)
|
|
|
|
adminAPI := api.Group("/admin", middleware.Auth())
|
|
{
|
|
posts := handler.NewAdminPostHandler(db)
|
|
adminAPI.GET("/stats", posts.Stats)
|
|
adminAPI.GET("/visits", handler.NewVisitHandler(db).Stats)
|
|
adminAPI.GET("/posts", posts.List)
|
|
adminAPI.POST("/posts", posts.Create)
|
|
adminAPI.GET("/posts/:id", posts.Get)
|
|
adminAPI.PUT("/posts/:id", posts.Update)
|
|
adminAPI.DELETE("/posts/:id", posts.Delete)
|
|
}
|
|
|
|
// ── 静态站点:/admin → 管理端,其余 → 用户端 ──
|
|
webDist, err := webfs.WebDist()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
adminDist, err := webfs.AdminDist()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
webServer := http.FileServer(http.FS(webDist))
|
|
adminServer := http.StripPrefix("/admin", http.FileServer(http.FS(adminDist)))
|
|
|
|
r.NoRoute(func(c *gin.Context) {
|
|
path := c.Request.URL.Path
|
|
if strings.HasPrefix(path, "/api/") {
|
|
resp.NotFound(c, "接口不存在")
|
|
return
|
|
}
|
|
if path == "/admin" || strings.HasPrefix(path, "/admin/") {
|
|
serveSPA(c, adminDist, adminServer, strings.TrimPrefix(path, "/admin"), "/admin/")
|
|
return
|
|
}
|
|
serveSPA(c, webDist, webServer, path, "/")
|
|
})
|
|
|
|
return r, nil
|
|
}
|
|
|
|
// serveSPA 真实文件直接吐,否则回退 index.html 交给前端路由。
|
|
func serveSPA(c *gin.Context, dist fs.FS, server http.Handler, rel, fallback string) {
|
|
p := strings.TrimPrefix(rel, "/")
|
|
if p != "" {
|
|
if _, err := fs.Stat(dist, p); err == nil {
|
|
server.ServeHTTP(c.Writer, c.Request)
|
|
return
|
|
}
|
|
}
|
|
c.Request.URL.Path = fallback
|
|
server.ServeHTTP(c.Writer, c.Request)
|
|
}
|