Files
jiang13-forum/handler/home_boot.go
freefire 2392768868 feat(ssr): 真 SSR 收尾——干净 hydration 与浏览器原生滚动恢复
- 修复 onRenderClient 引入 serverClient 导致浏览器 process is not defined:
  抽出双端安全的 ssr/feedSeed.ts 作为唯一数据播种入口,serverClient 加 SSR 守卫
- 修复 React 19 Fizz Suspense Outlining:Suspense 边界从布局 Outlet 下移到
  各懒加载路由,SSR 输出不再出现 <!--$?--> / $RB / template 探针标记
- 修复窄屏 React #418 hydration 致命错误:useMediaQuery 首帧恒 false,
  useLayoutEffect 绘制前同步真实值;主题改 cookie 存储供 SSR 直出,
  head 内联防闪烁/缩放脚本移除(缩放锁改为客户端 zoomGuard 模块)
- 清理假 SSR 残留:删除 StaticFeedList 镜像组件、SSR 图标生成脚本、
  探针与死代码;列表接口裁剪正文,仅下发 excerpt/first_image
- Feed 与帖子详情改为窗口滚动(app-shell--doc-scroll):删除虚拟列表、
  自研滚动快照与全局滚轮 hack;F5/前进后退滚动位置全部交给浏览器原生
  历史恢复,HTML 响应头 no-cache 配合;锚点/楼层用 scrollIntoView +
  scroll-margin-top 适配 sticky 顶栏
2026-09-09 20:53:28 +08:00

290 lines
7.5 KiB
Go
Raw Permalink 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 +data 取数与 SPA 冷启动预热共用,与前端 HomeBootPayload 对齐)。
// 并发聚合:帖子列表、板块、统计、侧栏 widget、登录态、签到等。
func (h *Handlers) APIHomeBoot(c *gin.Context) {
boardID := uint(parseUintOrZero(c.Query("board_id")))
boot, err := h.gatherHomeBoot(c, boardID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "获取数据失败"})
return
}
c.JSON(http.StatusOK, boot)
}
// gatherHomeBoot 纯数据聚合(JSON),不做任何 HTML 渲染。
func (h *Handlers) gatherHomeBoot(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
}