- 修复 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 顶栏
290 lines
7.5 KiB
Go
290 lines
7.5 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 +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
|
||
}
|