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 冷启动灌缓存
This commit is contained in:
288
handler/home_boot.go
Normal file
288
handler/home_boot.go
Normal file
@@ -0,0 +1,288 @@
|
||||
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
|
||||
}
|
||||
Reference in New Issue
Block a user