- 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 冷启动灌缓存
289 lines
7.4 KiB
Go
289 lines
7.4 KiB
Go
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
|
||
}
|