package web import ( "net/http" "strconv" "strings" "videoweb/app/service/authser" "videoweb/app/service/configser" "videoweb/app/service/playletser" "videoweb/app/service/seoser" "videoweb/app/service/translationser" "github.com/gin-gonic/gin" ) // HTML 渲染页面并注入 footer / 导航 / 公共配置 / 登录态(以 cookie 为准)。 func HTML(c *gin.Context, code int, name string, data gin.H) { if data == nil { data = gin.H{} } if _, ok := data["footer"]; !ok { footer := configser.FetchFooterOrEmpty(c) data["footer"] = footer } headerNav, ok := data["headerNav"].([]translationser.HeaderNavItemVM) if !ok { headerNav = translationser.FetchHeaderNavOrDefault(c) data["headerNav"] = headerNav } activeKey, _ := data["navActive"].(string) resolvedNavActive := translationser.ResolveNavigationActiveKey(activeKey, c.Request.URL.Path) headerNav = translationser.MarkHeaderNavActive(headerNav, c.Request.URL.Path) data["headerNav"] = headerNav data["resolvedNavActive"] = resolvedNavActive data["pageConsoleData"] = buildPageConsoleData(c.Request.URL.RequestURI(), code, name, data) seoInfo := pageSEOFromData(data["seoInfo"]) seoChanged := false if navSEO, found := translationser.FindHeaderNavSEO(headerNav, activeKey, c.Request.URL.Path); found { seoInfo = playletser.MergeSeoInfo(playletser.SeoInfo{ Title: navSEO.Title, Description: navSEO.Description, Keywords: navSEO.Keyworld, }, seoInfo) seoChanged = true } if selections, ok := data["seoSelections"].([]playletser.SEOSelection); ok && len(selections) > 0 { seoInfo = playletser.DifferentiateSeoInfo(seoInfo, selections) seoChanged = true } if pageNum := paginationPage(data["pagination"]); pageNum > 1 { pageTitle, _ := data["title"].(string) pageTitle = playletser.PaginatedTitle(pageTitle, pageNum) if pageTitle != "" { data["title"] = pageTitle } seoInfo = playletser.PaginateSeoInfo(seoInfo, paginationDescription(data, pageTitle), pageNum) seoChanged = true } if seoChanged { data["seoInfo"] = seoInfo if seoInfo.Title != "" { data["title"] = seoInfo.Title } } if _, ok := data["appConfig"]; !ok { data["appConfig"] = configser.FetchAppConfigVMOrEmpty(c) } cfg, ok := data["appConfig"].(configser.AppConfigVM) if !ok { cfg = configser.FetchAppConfigVMOrEmpty(c) } cfg = configser.ApplyTrackerFallback(cfg) data["appConfig"] = cfg siteURL := seoser.ResolveSiteURL(c, cfg) data["foreverUrlAddr"] = siteURL data["siteURL"] = siteURL attachPaginationLinks(data, siteURL) canonicalURL := seoser.CanonicalWithoutQuery(seoser.PageCanonicalURL(siteURL, cfg.ForeverURLAddr, c)) canonicalURL = seoser.ContentCanonicalURL(siteURL, canonicalURL, map[string]any(data)) data["canonicalURL"] = canonicalURL data["seoVerify"] = seoser.VerifyFromConfig() attachTrackerPage(c, code, data) noindex := pageNoIndex(data) if noindex { data["noindex"] = true } if code >= http.StatusOK && code < http.StatusMultipleChoices && !noindex && seoser.IsIndexablePath(c.Request.URL.Path) { data["jsonLD"] = seoser.PageJSONLD(siteURL, canonicalURL, c.Request.URL.RequestURI(), map[string]any(data), cfg) } // 无登录 token 时自动游客登录(POST /api/app/auth/guest),保证默认进入即有会话。 authser.EnsureGuestOnEnter(c) loggedIn, userName, avatar, email := authser.AuthFromCookies(c) registered := authser.IsRegisteredFromCookies(c) data["isLoggedIn"] = loggedIn data["isRegistered"] = registered if _, ok := data["isVip"]; !ok { data["isVip"] = loggedIn && authser.IsActiveVipFromCookies(c) } isVip, _ := data["isVip"].(bool) data["isVip"] = isVip if loggedIn { if userName != "" { data["userName"] = userName } if avatar != "" { data["avatar"] = avatar } if email != "" { data["email"] = email } if _, ok := data["userId"]; !ok { if uid := authser.UserIDFromCookies(c); uid > 0 { data["userId"] = authser.FormatUserID(uid) } } // 允许页面 handler 先注入最新钱包数据(如 getInfo),避免被 cookie 覆盖。 if _, ok := data["recharge"]; !ok { recharge := authser.RechargeFromCookies(c) data["recharge"] = recharge data["rechargeText"] = authser.FormatRecharge(recharge) } else if _, ok := data["rechargeText"]; !ok { data["rechargeText"] = authser.FormatRecharge(toInt64(data["recharge"])) } if _, ok := data["vipType"]; !ok { data["vipType"] = authser.VipTypeFromCookies(c) } vipType := toInt(data["vipType"]) data["vipType"] = vipType if _, ok := data["showCardName"]; !ok { data["showCardName"] = authser.ShowCardNameFromCookies(c) } showCardName, _ := data["showCardName"].(string) data["showCardName"] = strings.TrimSpace(showCardName) if _, ok := data["showCardId"]; !ok { data["showCardId"] = authser.ShowCardIDFromCookies(c) } if isVip { if _, ok := data["vipLabel"]; !ok { data["vipLabel"] = authser.VipDisplayLabel(showCardName, vipType) } if _, ok := data["vipExpireText"]; !ok { data["vipExpireText"] = authser.VipExpireTextFromCookies(c) } } else { delete(data, "vipLabel") delete(data, "vipExpireText") } } else { delete(data, "userName") delete(data, "avatar") delete(data, "email") delete(data, "userId") delete(data, "recharge") delete(data, "rechargeText") delete(data, "vipType") delete(data, "showCardId") delete(data, "showCardName") delete(data, "vipLabel") delete(data, "vipExpireText") } c.HTML(code, name, data) } // buildPageConsoleData 构建浏览器控制台使用的页面返回数据快照。 // 快照在公共配置和 cookie 登录态注入前生成,避免把会话相关公共字段写入日志。 func buildPageConsoleData(path string, code int, templateName string, data gin.H) gin.H { pageData := make(gin.H, len(data)) for key, value := range data { if pageConsoleExcludedKey(key) { continue } pageData[key] = value } payload := gin.H{ "path": path, "status": code, "template": templateName, "data": pageData, } if headerNav, ok := data["headerNav"]; ok { payload["headerNav"] = headerNav } if footer, ok := data["footer"]; ok { payload["footer"] = footer } return payload } func pageConsoleExcludedKey(key string) bool { switch key { case "pageConsoleData", "appConfig", "footer", "headerNav", "jsonLD", "seoVerify", "isLoggedIn", "isRegistered", "userName", "avatar", "email", "userId": return true default: return false } } func pageSEOFromData(value any) playletser.SeoInfo { switch seo := value.(type) { case playletser.SeoInfo: return seo case *playletser.SeoInfo: if seo != nil { return *seo } } return playletser.SeoInfo{} } func paginationPage(value any) int { switch pagination := value.(type) { case playletser.PaginationVM: return pagination.Page case *playletser.PaginationVM: if pagination != nil { return pagination.Page } } return 0 } func paginationDescription(data gin.H, pageTitle string) string { for _, key := range []string{"description", "categoryDescription", "tagDescription", "topicDescription"} { if value, _ := data[key].(string); strings.TrimSpace(value) != "" { return value } } pageTitle = strings.TrimSpace(pageTitle) if pageTitle == "" { return "浏览本页内容与站内相关推荐。" } return "浏览" + pageTitle + "的本页内容与站内相关推荐。" } func attachPaginationLinks(data gin.H, siteURL string) { var pagination playletser.PaginationVM switch value := data["pagination"].(type) { case playletser.PaginationVM: pagination = value case *playletser.PaginationVM: if value == nil { return } pagination = *value default: return } if !pagination.Visible { return } if pagination.HasPrev && strings.TrimSpace(pagination.PrevURL) != "" { data["paginationPrevURL"] = seoser.AbsURL(siteURL, pagination.PrevURL) } if pagination.HasNext && strings.TrimSpace(pagination.NextURL) != "" { data["paginationNextURL"] = seoser.AbsURL(siteURL, pagination.NextURL) } } // HTMLOK 200 快捷方法。 func HTMLOK(c *gin.Context, name string, data gin.H) { HTML(c, http.StatusOK, name, data) } // setNoIndex 统一设置页面级索引策略,并让公共渲染层同步关闭 JSON-LD。 func setNoIndex(data gin.H, follow bool) { if data == nil { return } data["noindex"] = true if follow { data["robots"] = "noindex,follow" return } data["robots"] = "noindex,nofollow" } func pageNoIndex(data gin.H) bool { if noindex, _ := data["noindex"].(bool); noindex { return true } robots, _ := data["robots"].(string) for _, directive := range strings.Split(strings.ToLower(robots), ",") { if strings.TrimSpace(directive) == "noindex" { return true } } return false } func toInt64(v any) int64 { switch n := v.(type) { case int64: return n case int: return int64(n) case int32: return int64(n) case float64: return int64(n) case string: i, _ := strconv.ParseInt(strings.TrimSpace(n), 10, 64) return i default: return 0 } } func toInt(v any) int { return int(toInt64(v)) }