Files
jiang13-forum/routers/web/home.go
freefire 145c7a3e1f feat: SSR 评论编辑与作者删除
帖详情提供时限内编辑与软删入口,作者删除与管理员共用回收站子树语义。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-29 21:36:12 +08:00

316 lines
11 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 web
import (
"errors"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"time"
"git.iioio.com/freefire/jiang13-forum/modules/auth"
"git.iioio.com/freefire/jiang13-forum/services"
"github.com/gin-gonic/gin"
)
// Register 注册已安装后的 web 路由
func Register(r *gin.Engine, deps Deps, authMW *auth.AuthMiddleware) {
g := r.Group("/", authMW.OptionalAuth())
g.GET("/", deps.Home)
g.GET("/board/:id", deps.Home)
g.GET("/post/:id", deps.PostView)
g.GET("/post/:id/edit", authMW.RequireAuth(), deps.PostEditGet)
g.POST("/post/:id/edit", authMW.RequireAuth(), deps.PostEditPost)
g.POST("/post/:id/comments", authMW.RequireAuth(), deps.PostComment)
g.GET("/post/:id/comments/:cid/edit", authMW.RequireAuth(), deps.CommentEditGet)
g.POST("/post/:id/comments/:cid/edit", authMW.RequireAuth(), deps.CommentEditPost)
g.POST("/post/:id/comments/:cid/delete", authMW.RequireAuth(), deps.CommentDeletePost)
g.POST("/post/:id/comments/:cid/like", authMW.RequireAuth(), deps.PostCommentLike)
g.POST("/post/:id/like", authMW.RequireAuth(), deps.PostLike)
g.POST("/post/:id/favorite", authMW.RequireAuth(), deps.PostFavorite)
g.POST("/post/:id/unlock", authMW.RequireAuth(), deps.PostUnlock)
g.POST("/post/:id/report", authMW.RequireAuth(), deps.PostReportPost)
g.POST("/post/:id/comments/:cid/report", authMW.RequireAuth(), deps.CommentReportPost)
g.POST("/post/:id/admin/pin", authMW.RequireAuth(), authMW.RequireAdmin(), deps.AdminPostPinPost)
g.POST("/post/:id/admin/board-pin", authMW.RequireAuth(), authMW.RequireAdmin(), deps.AdminPostBoardPinPost)
g.POST("/post/:id/admin/feature", authMW.RequireAuth(), authMW.RequireAdmin(), deps.AdminPostFeaturePost)
g.POST("/post/:id/admin/edit-lock", authMW.RequireAuth(), authMW.RequireAdmin(), deps.AdminPostEditLockPost)
g.POST("/post/:id/admin/comments-lock", authMW.RequireAuth(), authMW.RequireAdmin(), deps.AdminPostCommentsLockPost)
g.POST("/post/:id/admin/delete", authMW.RequireAuth(), authMW.RequireAdmin(), deps.AdminPostDeletePost)
g.GET("/login", deps.LoginGet)
g.POST("/login", deps.LoginPost)
g.POST("/logout", deps.LogoutPost)
g.GET("/register", deps.RegisterGet)
g.POST("/register", deps.RegisterPost)
g.POST("/register/send-code", deps.RegisterSendCode)
g.GET("/forgot-password", deps.ForgotPasswordGet)
g.POST("/forgot-password", deps.ForgotPasswordPost)
g.POST("/forgot-password/send-code", deps.ForgotPasswordSendCode)
g.GET("/compose", authMW.RequireAuth(), deps.ComposeGet)
g.POST("/compose", authMW.RequireAuth(), deps.ComposePost)
g.POST("/compose/upload", authMW.RequireAuth(), deps.ComposeUpload)
g.GET("/admin/login", func(c *gin.Context) { c.Redirect(http.StatusFound, "/login?redirect=/admin/dashboard") })
admin := g.Group("/admin", authMW.RequireAuth(), authMW.RequireAdmin())
{
admin.GET("", func(c *gin.Context) { c.Redirect(http.StatusFound, "/admin/dashboard") })
admin.GET("/dashboard", deps.AdminDashboard)
admin.GET("/boards", deps.AdminBoardsGet)
admin.POST("/boards", deps.AdminBoardCreate)
admin.POST("/boards/:id", deps.AdminBoardUpdate)
admin.POST("/boards/:id/delete", deps.AdminBoardDelete)
admin.GET("/moderation", deps.AdminModerationGet)
admin.POST("/posts/:id/approve", deps.AdminPostApprove)
admin.POST("/posts/:id/reject", deps.AdminPostReject)
admin.POST("/comments/:id/approve", deps.AdminCommentApprove)
admin.POST("/comments/:id/reject", deps.AdminCommentReject)
admin.GET("/settings", deps.AdminSettingsGet)
admin.POST("/settings/brand", deps.AdminSettingsBrandPost)
admin.POST("/settings/limits", deps.AdminSettingsLimitsPost)
admin.POST("/settings/filter-words", deps.AdminSettingsFilterWordsPost)
admin.POST("/settings/mail", deps.AdminSettingsMailPost)
admin.POST("/settings/mail/test", deps.AdminSettingsMailTestPost)
admin.POST("/settings/aside-widgets", deps.AdminSettingsAsideWidgetsPost)
admin.GET("/friend-links", deps.AdminFriendLinksGet)
admin.POST("/friend-links/settings", deps.AdminFriendLinksSettingsPost)
admin.POST("/friend-links/brand", deps.AdminFriendLinksBrandAddPost)
admin.POST("/friend-links/brand/delete", deps.AdminFriendLinksBrandDeletePost)
admin.POST("/friend-links/applies/:id/approve", deps.AdminFriendLinkApprovePost)
admin.POST("/friend-links/applies/:id/reject", deps.AdminFriendLinkRejectPost)
admin.GET("/pages", deps.AdminPagesGet)
admin.POST("/pages", deps.AdminPageCreate)
admin.GET("/pages/:id/edit", deps.AdminPageEditGet)
admin.POST("/pages/:id", deps.AdminPageUpdate)
admin.POST("/pages/:id/delete", deps.AdminPageDelete)
admin.POST("/pages/:id/publish", deps.AdminPagePublishPost)
admin.GET("/reports", deps.AdminReportsGet)
admin.POST("/reports/:id/handle", deps.AdminReportHandlePost)
admin.GET("/trash", deps.AdminTrashGet)
admin.POST("/trash/:id/restore", deps.AdminTrashRestorePost)
admin.POST("/trash/:id/purge", deps.AdminTrashPurgePost)
}
g.GET("/user/:id", deps.UserPublic)
g.GET("/profile", authMW.RequireAuth(), deps.ProfileGet)
g.POST("/profile/nickname", authMW.RequireAuth(), deps.ProfileNicknamePost)
g.POST("/profile/signature", authMW.RequireAuth(), deps.ProfileSignaturePost)
g.POST("/profile/password", authMW.RequireAuth(), deps.ProfilePasswordPost)
g.POST("/profile/avatar", authMW.RequireAuth(), deps.ProfileAvatarPost)
g.POST("/profile/checkin", authMW.RequireAuth(), deps.ProfileCheckInPost)
g.POST("/profile/lottery", authMW.RequireAuth(), deps.ProfileLotteryPost)
g.GET("/favorites", authMW.RequireAuth(), deps.FavoritesGet)
g.GET("/messages", authMW.RequireAuth(), deps.MessagesList)
g.GET("/messages/with/:peerId", authMW.RequireAuth(), deps.MessagesThread)
g.POST("/messages/with/:peerId", authMW.RequireAuth(), deps.MessagesSend)
g.POST("/messages/read-all", authMW.RequireAuth(), deps.MessagesReadAll)
g.GET("/links", deps.LinksGet)
g.POST("/links/apply", authMW.RequireAuth(), deps.LinksApplyPost)
g.POST("/links/apply/:id/cancel", authMW.RequireAuth(), deps.LinksApplyCancelPost)
g.POST("/links/logo", authMW.RequireAuth(), deps.LinksLogoUpload)
g.GET("/projects", deps.PendingPage)
g.GET("/boards", deps.BoardsGet)
g.GET("/page/:slug", deps.SitePageGet)
}
// HomePageData Feed
type HomePageData struct {
PageChrome
BoardName string
Sort string
Keyword string
Tag string
Author string
TitleOnly bool
HasSearch bool
SearchError string
Posts []PostListItem
Page int
PrevPage int
NextPage int
HasPrev bool
HasMore bool
}
// PostListItem 列表项
type PostListItem struct {
ID uint
Title string
AuthorName string
BoardName string
Pinned bool
Featured bool
CommentCount int
CreatedLabel string
}
// FormAction 搜索表单提交路径(当前 Feed
func (d HomePageData) FormAction() string {
if d.ActiveBoard > 0 {
return fmt.Sprintf("/board/%d", d.ActiveBoard)
}
return "/"
}
// SortHref 排序链接,保留搜索参数
func (d HomePageData) SortHref(sort string) string {
return buildFeedURL(d.ActiveBoard, sort, 0, d.Keyword, d.Tag, d.Author, d.TitleOnly)
}
// PageHref 分页链接,保留搜索与排序
func (d HomePageData) PageHref(page int) string {
return buildFeedURL(d.ActiveBoard, d.Sort, page, d.Keyword, d.Tag, d.Author, d.TitleOnly)
}
func buildFeedURL(boardID uint, sort string, page int, keyword, tag, author string, titleOnly bool) string {
q := url.Values{}
if sort != "" && sort != "latest" {
q.Set("sort", sort)
}
if page > 1 {
q.Set("page", strconv.Itoa(page))
}
if kw := strings.TrimSpace(keyword); kw != "" {
q.Set("keyword", kw)
}
if t := strings.TrimSpace(tag); t != "" {
q.Set("tag", t)
}
if a := strings.TrimSpace(author); a != "" {
q.Set("author", a)
}
if titleOnly {
q.Set("title_only", "1")
}
path := "/"
if boardID > 0 {
path = fmt.Sprintf("/board/%d", boardID)
}
if enc := q.Encode(); enc != "" {
return path + "?" + enc
}
return path
}
// Home 首页 / 板块
func (d Deps) Home(c *gin.Context) {
ctx := d.ctx(c)
sort := normalizeSort(c.DefaultQuery("sort", "latest"))
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
if page < 1 {
page = 1
}
size := d.Settings.PageSizeDefault()
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")
hasSearch := keyword != "" || tag != "" || author != ""
var boardID uint
var boardName string
if idStr := c.Param("id"); idStr != "" {
idStr = stripIDParam(idStr, d.Settings.Permalink().Ext)
if n, err := strconv.ParseUint(idStr, 10, 64); err == nil {
boardID = uint(n)
}
}
chrome := d.chrome(ctx, "", "", "home/feed")
chrome.ActiveBoard = boardID
for _, b := range chrome.Boards {
if b.ID == boardID {
boardName = b.Name
break
}
}
if boardName != "" {
chrome.Title = boardName + " · " + chrome.SiteName
}
data := HomePageData{
PageChrome: chrome,
BoardName: boardName,
Sort: sort,
Keyword: keyword,
Tag: tag,
Author: author,
TitleOnly: titleOnly,
HasSearch: hasSearch,
Page: page,
PrevPage: page - 1,
NextPage: page + 1,
HasPrev: page > 1,
}
listKW := keyword
if keyword != "" {
kw, err := d.Settings.NormalizeSearchKeyword(keyword)
if err != nil {
data.SearchError = err.Error()
data.Posts = []PostListItem{}
ctx.HTML(http.StatusOK, "home", data)
return
}
listKW = kw
data.Keyword = kw
}
items, total, err := d.Post.ListItems(services.PostListQuery{
BoardID: boardID,
Page: page,
Size: size,
Sort: sort,
Keyword: listKW,
Tag: tag,
Author: author,
TitleOnly: titleOnly,
ViewerID: ctx.UserID(),
ViewerIsAdmin: ctx.IsAdmin(),
})
if err != nil {
if errors.Is(err, services.ErrSearchKeywordTooShort) || errors.Is(err, services.ErrSearchKeywordTooLong) {
data.SearchError = err.Error()
data.Posts = []PostListItem{}
ctx.HTML(http.StatusOK, "home", data)
return
}
c.String(http.StatusInternalServerError, "加载帖子失败")
return
}
posts := make([]PostListItem, 0, len(items))
for _, it := range items {
authorName := strings.TrimSpace(it.User.Nickname)
if authorName == "" {
authorName = it.User.Username
}
bname := ""
if it.Board.ID > 0 {
bname = it.Board.Name
}
posts = append(posts, PostListItem{
ID: it.ID, Title: it.Title, AuthorName: authorName, BoardName: bname,
Pinned: it.Pinned, Featured: it.Featured, CommentCount: it.CommentCount,
CreatedLabel: it.CreatedAt.Local().Format("2006-01-02 15:04"),
})
}
data.Posts = posts
data.HasMore = int64(page*size) < total
ctx.HTML(http.StatusOK, "home", data)
}
func normalizeSort(s string) string {
switch s {
case "reply", "hot":
return s
default:
return "latest"
}
}
func formatTime(t time.Time) string {
return t.Local().Format("2006-01-02 15:04")
}