package web import ( "net/http" "strings" "git.iioio.com/freefire/jiang13-forum/modules/webctx" "git.iioio.com/freefire/jiang13-forum/services" "github.com/gin-gonic/gin" ) // Deps 页面依赖 type Deps struct { DataDir string JWTSecret string AssetVersion string // 静态资源 ?v=;来自构建 ldflags Settings *services.ForumSettingsService Auth *services.AuthService User *services.UserService Board *services.BoardService Post *services.PostService Comment *services.CommentService Message *services.MessageService Filter *services.SensitiveFilter Limiter *services.RateLimiter EmailCode *services.EmailCodeService Store *services.UploadStore Points *services.PointsService Badge *services.BadgeService FriendLink *services.FriendLinkApplyService Mail *services.MailService SitePage *services.SitePageService Report *services.ReportService Captcha *services.CaptchaService Notify *services.NotifyService Backup *services.BackupService } // SitePageLink 导航/页脚站点单页链接 type SitePageLink struct { Title string Slug string } // BoardView 侧栏 type BoardView struct { ID uint Name string Href string PostCount int ColorSlot int // 0–7 板块色槽 } // RightAsideHotPost 右栏热门帖 type RightAsideHotPost struct { ID uint Title string Href string } // RightAsideTag 右栏标签 type RightAsideTag struct { Name string Count int } // RightAsideComment 右栏最新评论 type RightAsideComment struct { PostID uint PostHref string Author string Excerpt string PostTitle string } // RightAsideUser 右栏最新用户 type RightAsideUser struct { ID uint Nickname string Href string } // RightAsideFriendLink 右栏友链 type RightAsideFriendLink struct { Name string URL string Logo string } // RightAsideWidget 按 aside_widgets 顺序的一块(仅已开启) type RightAsideWidget struct { Kind string // tag_cloud | recent_comments | recent_users | friend_links Tags []RightAsideTag Comments []RightAsideComment Users []RightAsideUser FriendLinks []RightAsideFriendLink } // RightAsideData 右栏:签到条 + 固定热门 + 可配置 widgets type RightAsideData struct { ShowCheckIn bool CheckedIn bool CheckInStreak int CheckInPoints int // 已签实得或预计可得 LotteryDrawn bool LotteryPoints int LotteryCost int HotPosts []RightAsideHotPost Widgets []RightAsideWidget } // PageChrome 布局公共字段 type PageChrome struct { Title string Description string SiteName string Slogan string LogoMark string LogoURL string FaviconURL string OGImageURL string PermalinkSuffix string // 如 ".html";未启用为空 OpenPostsInNewTab bool LoggedIn bool IsAdmin bool ViewerName string ViewerAvatar string ViewerMark string Boards []BoardView ActiveBoard uint Path string // 当前请求路径,供侧栏高亮 Inner string // 保留字段;入口模板已固定组合,不再动态 template CSRF string Flash string Error string UnreadCount int64 // 登录用户未读私信/通知总数;未登录为 0 ViewerPoints int // 登录用户当前积分;未登录为 0 ShowFriendLinksNav bool ShowFriendLinksFooter bool NavPages []SitePageLink FooterPages []SitePageLink RightAside RightAsideData AssetVersion string } func (d Deps) ctx(c *gin.Context) *webctx.Context { return webctx.New(c, d.JWTSecret) } func (d Deps) chrome(ctx *webctx.Context, title, desc, inner string) PageChrome { brand := d.Settings.SiteBranding() if title == "" { title = brand.DocumentTitle() } if desc == "" { desc = brand.MetaDescription() } name := "我的" if ctx.IsSigned() { name = strings.TrimSpace(ctx.Doer.Nickname) if name == "" { name = ctx.Doer.Username } } boards, _ := d.Board.ListWithStats() bv := make([]BoardView, 0, len(boards)) pl := d.permalink() for _, b := range boards { slot := b.ColorIndex if slot < 0 { slot = int(b.ID % 8) } bv = append(bv, BoardView{ ID: b.ID, Name: b.Name, Href: pl.BoardPath(b.ID), PostCount: b.PostCount, ColorSlot: slot % 8, }) } var unread int64 if ctx.IsSigned() && d.Message != nil { unread, _ = d.Message.UnreadCount(ctx.UserID()) } viewerPoints := 0 viewerAvatar := "" viewerMark := "我" if ctx.IsSigned() && ctx.Doer != nil { viewerPoints = ctx.Doer.Points viewerAvatar = strings.TrimSpace(ctx.Doer.Avatar) viewerMark = firstRuneOr(name, "我") } path := "" if ctx.C != nil && ctx.C.Request != nil && ctx.C.Request.URL != nil { path = ctx.C.Request.URL.Path } navPages, footerPages := d.sitePageLinks() assetVer := strings.TrimSpace(d.AssetVersion) if assetVer == "" { assetVer = "dev" } return PageChrome{ Title: title, Description: desc, SiteName: brand.Name, Slogan: brand.Slogan, LogoMark: firstRuneOr(brand.LogoMark, "姜"), LogoURL: strings.TrimSpace(brand.Logo), FaviconURL: strings.TrimSpace(brand.Favicon), OGImageURL: strings.TrimSpace(brand.DefaultShareImage()), PermalinkSuffix: d.Settings.Permalink().Suffix(), OpenPostsInNewTab: d.Settings.OpenPostsInNewTab(), LoggedIn: ctx.IsSigned(), IsAdmin: ctx.IsAdmin(), ViewerName: name, ViewerAvatar: viewerAvatar, ViewerMark: viewerMark, Boards: bv, Path: path, Inner: inner, CSRF: ctx.EnsureCSRF(), Flash: ctx.TakeFlash(), UnreadCount: unread, ViewerPoints: viewerPoints, ShowFriendLinksNav: d.Settings.NavShowFriendLinks(), ShowFriendLinksFooter: d.Settings.FooterShowFriendLinks(), NavPages: navPages, FooterPages: footerPages, RightAside: d.loadRightAside(ctx, brand), AssetVersion: assetVer, } } func (d Deps) sitePageLinks() (nav, footer []SitePageLink) { nav, footer = []SitePageLink{}, []SitePageLink{} if d.SitePage == nil { return } list, err := d.SitePage.ListPublished() if err != nil { return } for _, p := range list { link := SitePageLink{Title: p.Title, Slug: p.Slug} if p.ShowInNav { nav = append(nav, link) } if p.ShowInFooter { footer = append(footer, link) } } return } const ( rightAsideHotLimit = 5 rightAsideTagLimit = 24 rightAsideCommentLimit = 8 rightAsideUserLimit = 8 ) func (d Deps) loadRightAside(ctx *webctx.Context, brand services.SiteBranding) RightAsideData { out := RightAsideData{ HotPosts: []RightAsideHotPost{}, Widgets: []RightAsideWidget{}, } if ctx.IsSigned() && d.Points != nil { out.ShowCheckIn = true if st, err := d.Points.GetCheckInStatus(ctx.UserID()); err == nil { out.CheckedIn = st.CheckedIn out.CheckInStreak = st.Streak out.CheckInPoints = st.TodayPoints } if st, err := d.Points.GetLotteryStatus(ctx.UserID()); err == nil { out.LotteryDrawn = st.Drawn out.LotteryPoints = st.Points out.LotteryCost = st.Cost } } if d.Post != nil { if items, err := d.Post.HotPosts(rightAsideHotLimit); err == nil { pl := d.permalink() for _, it := range items { out.HotPosts = append(out.HotPosts, RightAsideHotPost{ID: it.ID, Title: it.Title, Href: pl.PostPath(it.ID)}) } } } for _, w := range d.Settings.AsideWidgets() { if !w.Enabled { continue } block := RightAsideWidget{Kind: w.ID} switch w.ID { case services.AsideWidgetTagCloud: if d.Post != nil { if tags, err := d.Post.PopularTags(rightAsideTagLimit); err == nil { for _, t := range tags { block.Tags = append(block.Tags, RightAsideTag{Name: t.Name, Count: t.Count}) } } } case services.AsideWidgetRecentComments: if d.Comment != nil { if list, err := d.Comment.ListRecentPublic(rightAsideCommentLimit); err == nil { pl := d.permalink() for _, c := range list { block.Comments = append(block.Comments, RightAsideComment{ PostID: c.PostID, PostHref: pl.PostPath(c.PostID), Author: c.Author, Excerpt: c.Excerpt, PostTitle: c.PostTitle, }) } } } case services.AsideWidgetRecentUsers: if d.User != nil { if list, err := d.User.ListRecentRegistered(rightAsideUserLimit); err == nil { pl := d.permalink() for _, u := range list { block.Users = append(block.Users, RightAsideUser{ID: u.ID, Nickname: u.Nickname, Href: pl.UserPath(u.ID)}) } } } case services.AsideWidgetFriendLinks: for _, l := range brand.FriendLinks { block.FriendLinks = append(block.FriendLinks, RightAsideFriendLink{Name: l.Name, URL: l.URL, Logo: l.Logo}) } default: continue } out.Widgets = append(out.Widgets, block) } return out } func firstRuneOr(s, fallback string) string { s = strings.TrimSpace(s) if s == "" { return fallback } for _, r := range s { return string(r) } return fallback } func stripIDParam(raw, permalinkExt string) string { raw = strings.TrimSpace(raw) if permalinkExt != "" { raw = strings.TrimSuffix(raw, "."+permalinkExt) } raw = strings.TrimSuffix(raw, ".html") raw = strings.TrimSuffix(raw, ".htm") return raw } func (d Deps) permalink() services.PermalinkConfig { if d.Settings == nil { return services.PermalinkConfig{} } return d.Settings.Permalink() } func (d Deps) postHref(id uint) string { return d.permalink().PostPath(id) } func (d Deps) boardHref(id uint) string { return d.permalink().BoardPath(id) } func (d Deps) userHref(id uint) string { return d.permalink().UserPath(id) } // redirectIfNotCanonical 伪静态开启时把无后缀/错误后缀 301 到规范路径 func (d Deps) redirectIfNotCanonical(c *gin.Context, match services.PermalinkMatch) bool { if !match.OK || !d.permalink().Enabled { return false } path := c.Request.URL.Path if !match.NeedsCanonicalRedirect(path) { return false } loc := match.Canonical if q := c.Request.URL.RawQuery; q != "" { loc = loc + "?" + q } c.Redirect(http.StatusMovedPermanently, loc) return true }