Files
jiang13-forum/handler/home_boot.go
freefire c99238c1e7 feat: Vike +data 服务端取数 / SEO Head 移植 / 404 错误页 / 服务端 DOM 模拟
- feedData.ts 聚合首页/板块/帖子详情数据,缺失内容 throw render(404)
- Go 侧 spa_meta/seo 的 head 生成逻辑移植到 src/ssr/head.ts
- _error 页复用站点壳渲染 404,+data 从 abortReason 恢复壳层数据
- linkedom 模拟服务端 DOM,DOMPurify 封装为 SSR 透传的 sanitizeHtml
- 新增 /api/home-boot 聚合端点(handler/home_boot.go)供 SSR 冷启动灌缓存
2026-09-09 02:41:30 +08:00

289 lines
7.4 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package handler
import (
"net/http"
"strconv"
"strings"
"sync"
"git.iioio.com/freefire/jiang13-forum/model"
"git.iioio.com/freefire/jiang13-forum/service"
"github.com/gin-gonic/gin"
)
// homeBootPayload 与前端 HomeBootPayload 对齐(Vike +data 冷启动灌缓存)。
type homeBootPayload struct {
BoardID uint `json:"board_id"`
Sort string `json:"sort"`
Keyword string `json:"keyword"`
Tag string `json:"tag"`
Author string `json:"author"`
TitleOnly bool `json:"title_only"`
Posts []service.PostListItem `json:"posts"`
PostTotal int64 `json:"post_total"`
Page int `json:"page"`
Boards []service.BoardWithStats `json:"boards"`
Stats homeBootStats `json:"stats"`
RecentComments []service.RecentCommentItem `json:"recent_comments"`
RecentUsers []service.RecentUserItem `json:"recent_users"`
Tags []service.TagCount `json:"tags"`
// omitempty:侧栏关闭或未拉取时不写字段,避免 SPA 把 [] 当成有效会话快照
Showcase []service.CommunityShowcaseItem `json:"showcase,omitempty"`
Pages []service.SitePageSummary `json:"pages"`
Limits service.ForumLimitsPublic `json:"limits"`
Branding service.SiteBranding `json:"branding"`
User *model.UserSelf `json:"user"`
UnreadMessages int `json:"unread_messages"`
CheckIn *service.CheckInStatus `json:"check_in"`
}
type homeBootStats struct {
Users int64 `json:"users"`
Posts int64 `json:"posts"`
Boards int64 `json:"boards"`
Comments int64 `json:"comments"`
}
// APIHomeBoot 首页/板块/壳层聚合数据(Vike SSR +data 使用,与前端 HomeBootPayload 对齐)。
// 并发聚合:帖子列表、板块、统计、侧栏 widget、登录态、签到等。
func (h *Handlers) APIHomeBoot(c *gin.Context) {
boardID := uint(parseUintOrZero(c.Query("board_id")))
boot, err := h.gatherHomeSSR(c, boardID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取数据失败"})
return
}
c.JSON(http.StatusOK, boot)
}
func (h *Handlers) gatherHomeSSR(c *gin.Context, boardID uint) (*homeBootPayload, error) {
limits := h.Settings.PublicLimits()
brand := h.Settings.SiteBranding()
brand.SiteURL = h.publicBaseURL(c)
defSort := h.Settings.DefaultFeedSort()
sort := strings.TrimSpace(c.Query("sort"))
if sort == "" {
sort = defSort
}
keyword := strings.TrimSpace(c.Query("keyword"))
tag := strings.TrimSpace(c.Query("tag"))
author := strings.TrimSpace(c.Query("author"))
titleOnly := c.Query("title_only") == "1" || strings.EqualFold(c.Query("title_only"), "true")
if tag != "" {
keyword = ""
author = ""
titleOnly = false
}
pageSize := h.Settings.PageSizeDefault()
out := &homeBootPayload{
BoardID: boardID,
Sort: sort,
Keyword: keyword,
Tag: tag,
Author: author,
TitleOnly: titleOnly,
Page: 1,
Posts: []service.PostListItem{},
Boards: []service.BoardWithStats{},
Pages: []service.SitePageSummary{},
Limits: limits,
Branding: brand,
}
var (
postsErr, boardsErr error
wg sync.WaitGroup
)
wg.Add(1)
go func() {
defer wg.Done()
q := service.PostListQuery{
BoardID: boardID,
Page: 1,
Size: pageSize,
Sort: sort,
Keyword: keyword,
Tag: tag,
Author: author,
TitleOnly: titleOnly,
ViewerID: h.currentUserID(c),
ViewerIsAdmin: h.isAdmin(c),
}
items, total, err := h.Post.ListItems(q)
if err != nil {
postsErr = err
return
}
if items == nil {
items = []service.PostListItem{}
}
if h.Badge != nil {
users := make([]*model.User, 0, len(items))
for i := range items {
if items[i].User.ID > 0 {
users = append(users, &items[i].User)
}
}
h.Badge.AttachBadgeSummaries(users, 2)
}
out.Posts = items
out.PostTotal = total
}()
wg.Add(1)
go func() {
defer wg.Done()
boards, err := h.Board.ListWithStats()
if err != nil {
boardsErr = err
return
}
if boards == nil {
boards = []service.BoardWithStats{}
}
out.Boards = boards
}()
wg.Add(1)
go func() {
defer wg.Done()
var users, posts, boardsN, comments int64
model.DB.Model(&model.User{}).Count(&users)
model.DB.Model(&model.Post{}).Where("status = ?", model.ContentStatusPublished).Count(&posts)
model.DB.Model(&model.Board{}).Count(&boardsN)
model.DB.Model(&model.Comment{}).Where("status = ?", model.ContentStatusPublished).Count(&comments)
out.Stats = homeBootStats{Users: users, Posts: posts, Boards: boardsN, Comments: comments}
}()
wg.Add(1)
go func() {
defer wg.Done()
pages, err := h.SitePage.ListPublished()
if err != nil || pages == nil {
pages = []service.SitePageSummary{}
}
out.Pages = pages
}()
widgetEnabled := map[string]bool{}
for _, w := range limits.AsideWidgets {
widgetEnabled[w.ID] = w.Enabled
}
if widgetEnabled[service.AsideWidgetRecentComments] {
wg.Add(1)
go func() {
defer wg.Done()
items, err := h.Comment.ListRecentPublic(8)
if err != nil || items == nil {
items = []service.RecentCommentItem{}
}
out.RecentComments = items
}()
} else {
out.RecentComments = []service.RecentCommentItem{}
}
if widgetEnabled[service.AsideWidgetRecentUsers] {
wg.Add(1)
go func() {
defer wg.Done()
items, err := h.User.ListRecentRegistered(8)
if err != nil || items == nil {
items = []service.RecentUserItem{}
}
out.RecentUsers = items
}()
} else {
out.RecentUsers = []service.RecentUserItem{}
}
if widgetEnabled[service.AsideWidgetTagCloud] {
wg.Add(1)
go func() {
defer wg.Done()
tags, err := h.Post.PopularTags(40)
if err != nil || tags == nil {
tags = []service.TagCount{}
}
out.Tags = tags
}()
} else {
out.Tags = []service.TagCount{}
}
if widgetEnabled[service.AsideWidgetShowcase] && h.Community != nil {
wg.Add(1)
go func() {
defer wg.Done()
items, err := h.Community.ListShowcase(c.Request.Host)
if err != nil {
// 失败不写 showcase,让前端自行请求,避免假空粘死
return
}
if items == nil {
items = []service.CommunityShowcaseItem{}
}
out.Showcase = items
}()
}
uid := h.currentUserID(c)
if uid > 0 {
wg.Add(1)
go func() {
defer wg.Done()
user, err := h.User.GetByID(uid)
if err != nil || user == nil {
return
}
view := user.ToSelf()
if h.Badge != nil {
_ = h.Badge.EvaluateAuto(user.ID)
if badges, bErr := h.Badge.ListUserBadges(user.ID); bErr == nil {
view.Badges = service.BadgeViews(badges, 0)
}
}
out.User = &view
}()
if h.Message != nil {
wg.Add(1)
go func() {
defer wg.Done()
total, _, _, err := h.Message.UnreadCounts(uid)
if err == nil {
out.UnreadMessages = int(total)
}
}()
}
if h.Points != nil {
wg.Add(1)
go func() {
defer wg.Done()
st, err := h.Points.GetCheckInStatus(uid)
if err == nil {
out.CheckIn = &st
}
}()
}
}
wg.Wait()
if postsErr != nil {
return nil, postsErr
}
if boardsErr != nil {
return nil, boardsErr
}
return out, nil
}
func parseUintOrZero(s string) uint64 {
n, _ := strconv.ParseUint(s, 10, 64)
return n
}