Files
web_hgtv_h5/app/router/gin.go
T
2026-10-29 20:45:09 +07:00

279 lines
8.0 KiB
Go

package router
import (
"encoding/json"
"html/template"
"net/http"
"os"
"path/filepath"
"strings"
"videoweb/app/api/web"
"videoweb/app/appg"
"videoweb/app/service/playletser"
"videoweb/app/service/seoser"
"github.com/gin-gonic/gin"
)
func Init() *gin.Engine {
setGinMode()
engine := gin.Default()
engine.HandleMethodNotAllowed = true
setupEngine(engine)
engine.NoRoute(renderNotFound)
engine.NoMethod(renderNotFound)
registerM3U8Routes(engine)
registerRoutes(engine)
return engine
}
// DHInit 导航站独立端口:仅暴露官方导航页及静态资源。
func DHInit() *gin.Engine {
setGinMode()
engine := gin.Default()
engine.HandleMethodNotAllowed = true
setupEngine(engine)
engine.StaticFile("/favicon.ico", "./static/favicon.ico")
engine.StaticFile("/favicon.svg", "./static/favicon.svg")
engine.StaticFile("/favicon.png", "./static/favicon.png")
engine.GET("/ui/dialog/:name", web.DialogFragment)
// 导航页同样展示弹窗 / 九宫格广告,需要点击上报接口。
engine.POST("/api/app/ads/click", web.AdsClick)
engine.GET("/", web.Nav)
engine.GET("/nav/", web.Nav)
engine.GET("/dh/", web.Nav)
engine.NoRoute(renderNotFound)
engine.NoMethod(renderNotFound)
return engine
}
func setGinMode() {
switch strings.ToLower(strings.TrimSpace(appg.Conf.RunMod)) {
case "test":
gin.SetMode(gin.TestMode)
case "prod":
gin.SetMode(gin.ReleaseMode)
default:
gin.SetMode(gin.DebugMode)
}
}
func setupEngine(engine *gin.Engine) {
tplFuncs := playletser.TemplateFuncs()
tplFuncs["InJSONScript"] = inJSONScript
tplFuncs["jsonStr"] = jsonStr
engine.SetFuncMap(tplFuncs)
engine.LoadHTMLFiles(listHTMLTemplates("templates")...)
engine.Use(staticCacheHeaders(), crawlerHeaders())
registerBrowserDiscoveryRoutes(engine)
engine.Static("/static", "./static")
}
// registerBrowserDiscoveryRoutes 处理 Chrome DevTools 的本地工作区探测请求。
// 该请求并非站点页面,返回 204 可避免进入业务 404 页面和产生误导日志。
func registerBrowserDiscoveryRoutes(engine *gin.Engine) {
engine.GET("/.well-known/appspecific/com.chrome.devtools.json", func(c *gin.Context) {
c.Header("Cache-Control", "public, max-age=86400")
c.Header("X-Robots-Tag", "noindex, nofollow")
c.Status(http.StatusNoContent)
})
}
// staticCacheHeaders 为静态资源设置浏览器缓存,减少重复下载。
func staticCacheHeaders() gin.HandlerFunc {
return func(c *gin.Context) {
if strings.HasPrefix(c.Request.URL.Path, "/static/") {
c.Header("Cache-Control", "public, max-age=604800, stale-while-revalidate=86400")
}
c.Next()
}
}
// crawlerHeaders 防止接口、片段和健康检查等非 HTML 内容进入搜索索引。
func crawlerHeaders() gin.HandlerFunc {
return func(c *gin.Context) {
path := c.Request.URL.Path
if strings.HasPrefix(path, "/api/") ||
strings.HasPrefix(path, "/ui/") ||
strings.HasPrefix(path, "/h5/") ||
path == "/healthz" || path == "/enkey" || path == "/newenkey" {
c.Header("X-Robots-Tag", "noindex, nofollow")
}
c.Next()
}
}
// listHTMLTemplates 收集 templates 下页面模板,跳过遗留广告文件。
func listHTMLTemplates(root string) []string {
var files []string
_ = filepath.WalkDir(root, func(path string, d os.DirEntry, err error) error {
if err != nil || d.IsDir() {
return err
}
name := strings.ToLower(d.Name())
if !strings.HasSuffix(name, ".html") {
return nil
}
if name == "banners.html" {
return nil
}
files = append(files, path)
return nil
})
if len(files) == 0 {
panic("no html templates found under " + root)
}
return files
}
// inJSONScript 将任意数据序列化为可嵌入 <script> 的 JSON。
func inJSONScript(v any) template.JS {
b, err := json.Marshal(v)
if err != nil {
return template.JS("null")
}
return template.JS(b)
}
// jsonStr 将字符串序列化为可嵌入 application/ld+json 的 JSON 字符串字面量。
func jsonStr(s string) template.JS {
b, err := json.Marshal(s)
if err != nil {
return template.JS(`""`)
}
return template.JS(b)
}
func registerRoutes(r *gin.Engine) {
r.GET("/", web.Index)
r.GET("/ui/dialog/:name", web.DialogFragment)
r.POST("/api/feedback/add", web.AddFeedback)
r.POST("/api/contactus/add", web.AddContactUs)
r.GET("/api/comment/list", web.CommentList)
r.POST("/api/playlet/forward/heat", web.PlayletForwardHeat)
r.POST("/api/app/ads/click", web.AdsClick)
r.GET("/api/playlet/chapter/detail", web.ChapterDetail)
r.GET("/api/playlet/chapter/list", web.ChapterList)
r.GET("/api/playlet/operate/list", web.OperateList)
r.GET("/api/playlet/hot-like", web.HotLike)
r.GET("/api/app/playlet/hot-like", web.HotLike)
r.GET("/list/", web.List)
r.GET("/list/:slug/", web.List)
r.GET("/list/:slug/:sortOrder/", web.List)
r.GET("/list/:slug/:sortOrder/page/:page/", web.List)
r.GET("/series/", web.Series)
r.GET("/series/page/:page/", web.Series)
r.GET("/series/:filter/", web.Series)
r.GET("/series/:filter/page/:page/", web.Series)
// 标签页面使用 tag 路由;tag-list 仅作为历史地址保留并由 handler 301 到 canonical。
r.GET("/tag/", web.TagList)
r.GET("/tag/page/:page/", web.TagList)
r.GET("/tag/:slug/", web.TagList)
r.GET("/tag/:slug/page/:page/", web.TagList)
r.GET("/tag-list/", web.TagList)
r.GET("/tag-list/page/:page/", web.TagList)
r.GET("/tag-list/:slug/", web.TagList)
r.GET("/tag-list/:slug/page/:page/", web.TagList)
r.GET("/rank/", web.Rank)
r.GET("/rank/page/:page/", web.Rank)
r.GET("/rank/:sortOrder/", web.Rank)
r.GET("/rank/:sortOrder/page/:page/", web.Rank)
r.GET("/today-update/", web.TodayUpdate)
r.GET("/today-update/page/:page/", web.TodayUpdate)
// 分类详情使用语义化 category 路由,分类标识优先使用 slug、无 slug 时使用 ID。
r.GET("/category/:slug/", web.CategoryDetail)
r.GET("/category/:slug/page/:page/", web.CategoryDetail)
// 专题列表和详情统一使用 topic 路由;more 仅作为历史地址保留并由 handler 301 到 canonical。
r.GET("/topic/", web.ThemeIndex)
r.GET("/topic/page/:page/", web.ThemeIndex)
r.GET("/topic/:slug/", web.More)
r.GET("/topic/:slug/page/:page/", web.More)
r.GET("/more/", web.More)
r.GET("/more/page/:page/", web.More)
r.GET("/more/:slug/", web.More)
r.GET("/more/:slug/page/:page/", web.More)
r.GET("/download-app/", func(c *gin.Context) {
web.HTMLOK(c, "downloadApp.html", gin.H{
"title": "下载APP",
"navActive": "downloadApp",
})
})
r.GET("/home-road/", web.HomeRoad)
r.GET("/detail/:slug/", web.Detail)
r.GET("/play/:slug/", web.Play)
r.GET("/play/:slug/:chapterId/", web.Play)
r.GET("/search/", web.Search)
r.GET("/search/:keyword/", web.Search)
r.GET("/search/:keyword/page/:page/", web.Search)
r.GET("/about-us/", web.RichtextPage("关于我们"))
r.GET("/user-agreement/", web.RichtextPage("用户协议"))
r.GET("/privacy-policy/", web.RichtextPage("隐私政策"))
r.GET("/help-center/", web.RichtextPage("帮助中心"))
r.GET("/richtext/", web.Richtext)
r.GET("/richtext/:title/", web.Richtext)
r.GET("/feedback/", func(c *gin.Context) {
web.HTMLOK(c, "feedback.html", gin.H{
"title": "问题反馈",
"noindex": true,
"robots": "noindex,nofollow",
})
})
r.GET("/nav/", web.Nav)
r.GET("/healthz", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "ok",
})
})
registerSitemapRoutes(r)
r.GET("/robots.txt", web.RobotsTXT)
registerSEOVerifyRoutes(r)
r.GET("/favicon.ico", func(c *gin.Context) {
c.File("./static/favicon.ico")
})
r.GET("/favicon.svg", func(c *gin.Context) {
c.File("./static/favicon.svg")
})
r.GET("/favicon.png", func(c *gin.Context) {
c.File("./static/favicon.png")
})
}
func renderNotFound(c *gin.Context) {
web.HTML(c, http.StatusNotFound, "404.html", gin.H{
"path": c.Request.URL.Path,
"noindex": true,
"robots": "noindex,follow",
})
}
func registerSEOVerifyRoutes(r *gin.Engine) {
codes := seoser.VerifyFromConfig()
if codes.BaiduHTML != "" {
r.GET("/"+codes.BaiduHTML, web.SEOVerifyFile)
}
if codes.IndexNowKey != "" {
r.GET("/"+codes.IndexNowKey+".txt", web.SEOVerifyFile)
}
}