feat: SSR 帖/评举报与 Admin 处理

EOF

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
2026-08-29 17:47:09 +08:00
parent 802f86605c
commit 83994e2707
16 changed files with 465 additions and 13 deletions

View File

@@ -106,6 +106,7 @@ func Setup(cfg *config.Config) (*gin.Engine, error) {
FriendLink: friendLinkApplySvc,
Mail: mailSvc,
SitePage: sitePageSvc,
Report: reportSvc,
}, authMW)
r.GET("/media/thumb/*filepath", h.ServeImageThumb)

View File

@@ -32,11 +32,12 @@ func (d Deps) adminChrome(ctx *webctx.Context, title, nav string) AdminChrome {
type adminDashData struct {
AdminChrome
UserCount int64
PostCount int64
PendingPosts int64
PendingComments int64
BoardCount int64
UserCount int64
PostCount int64
PendingPosts int64
PendingComments int64
PendingReports int64
BoardCount int64
}
// AdminDashboard 概览
@@ -48,12 +49,17 @@ func (d Deps) AdminDashboard(c *gin.Context) {
_ = models.DB.Model(&models.Board{}).Count(&boards).Error
pendingPosts, _ := d.Post.PendingPostCount()
pendingComments, _ := d.Comment.PendingCommentCount()
var pendingReports int64
if d.Report != nil {
pendingReports, _ = d.Report.PendingCount()
}
ctx.HTML(http.StatusOK, "admin/dashboard", adminDashData{
AdminChrome: d.adminChrome(ctx, "仪表盘", "dashboard"),
UserCount: users,
PostCount: posts,
PendingPosts: pendingPosts,
PendingComments: pendingComments,
PendingReports: pendingReports,
BoardCount: boards,
})
}

View File

@@ -0,0 +1,143 @@
package web
import (
"net/http"
"strconv"
"strings"
"git.iioio.com/freefire/jiang13-forum/models"
"git.iioio.com/freefire/jiang13-forum/modules/webctx"
"git.iioio.com/freefire/jiang13-forum/services"
"github.com/gin-gonic/gin"
)
type adminReportRow struct {
ID uint
CreatedAt string
Status string
StatusLabel string
ReasonLabel string
Detail string
ReporterName string
TargetLabel string
PostID uint
IsComment bool
Pending bool
}
type adminReportsData struct {
AdminChrome
Reports []adminReportRow
PendingCount int64
StatusFilter string
Page int
HasPrev bool
HasMore bool
PrevPage int
NextPage int
}
// AdminReportsGet 举报列表
func (d Deps) AdminReportsGet(c *gin.Context) {
ctx := d.ctx(c)
d.renderAdminReports(ctx, "")
}
func (d Deps) renderAdminReports(ctx *webctx.Context, errMsg string) {
chrome := d.adminChrome(ctx, "举报", "reports")
chrome.Error = errMsg
status := strings.TrimSpace(ctx.C.DefaultQuery("status", "pending"))
if status == "" {
status = "pending"
}
page, _ := strconv.Atoi(ctx.C.DefaultQuery("page", "1"))
if page < 1 {
page = 1
}
data := adminReportsData{
AdminChrome: chrome,
StatusFilter: status,
Page: page,
PrevPage: page - 1,
NextPage: page + 1,
HasPrev: page > 1,
}
if d.Report != nil {
data.PendingCount, _ = d.Report.PendingCount()
list, total, _ := d.Report.ListAdmin(services.ReportListQuery{
Status: status, Page: page, Size: d.Settings.PageSizeDefault(),
})
data.HasMore = int64(page*d.Settings.PageSizeDefault()) < total
data.Reports = make([]adminReportRow, 0, len(list))
for _, r := range list {
row := adminReportRow{
ID: r.ID, CreatedAt: r.CreatedAt.Format("2006-01-02 15:04"),
Status: r.Status, StatusLabel: reportStatusLabel(r.Status),
ReasonLabel: services.ReportReasonLabel(r.Reason), Detail: r.Detail,
PostID: r.PostID, Pending: r.Status == models.ReportStatusPending,
}
if r.Reporter.ID > 0 {
row.ReporterName = r.Reporter.Username
}
isComment := r.CommentID != nil && *r.CommentID > 0
row.IsComment = isComment
title := ""
if r.Post.ID > 0 {
title = r.Post.Title
}
if isComment {
floor := 0
if r.Comment != nil {
floor = r.Comment.Floor
}
row.TargetLabel = "评论 #" + strconv.Itoa(floor) + " · 《" + title + "》"
} else {
row.TargetLabel = "帖子 · 《" + title + "》"
}
data.Reports = append(data.Reports, row)
}
}
ctx.HTML(http.StatusOK, "admin/reports", data)
}
// AdminReportHandlePost 处理举报
func (d Deps) AdminReportHandlePost(c *gin.Context) {
ctx := d.ctx(c)
if !ctx.CheckCSRF() {
ctx.SetFlash("无效请求,请重试")
ctx.Redirect("/admin/reports")
return
}
if d.Report == nil {
ctx.SetFlash("举报服务未就绪")
ctx.Redirect("/admin/reports")
return
}
id, _ := strconv.ParseUint(c.Param("id"), 10, 64)
action := strings.TrimSpace(c.PostForm("action"))
if _, err := d.Report.Handle(services.HandleReportInput{
ReportID: uint(id),
HandlerID: ctx.UserID(),
Action: action,
HandleNote: strings.TrimSpace(c.PostForm("handle_note")),
RejectReason: strings.TrimSpace(c.PostForm("reject_reason")),
}); err != nil {
d.renderAdminReports(ctx, err.Error())
return
}
ctx.SetFlash("举报已处理")
ctx.Redirect("/admin/reports?status=pending")
}
func reportStatusLabel(s string) string {
switch s {
case models.ReportStatusPending:
return "待处理"
case models.ReportStatusResolved:
return "已处理"
case models.ReportStatusDismissed:
return "已驳回"
default:
return s
}
}

View File

@@ -27,6 +27,7 @@ type Deps struct {
FriendLink *services.FriendLinkApplyService
Mail *services.MailService
SitePage *services.SitePageService
Report *services.ReportService
}
// SitePageLink 导航/页脚站点单页链接

View File

@@ -27,6 +27,8 @@ func Register(r *gin.Engine, deps Deps, authMW *auth.AuthMiddleware) {
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.GET("/login", deps.LoginGet)
g.POST("/login", deps.LoginPost)
g.POST("/logout", deps.LogoutPost)
@@ -72,6 +74,8 @@ func Register(r *gin.Engine, deps Deps, authMW *auth.AuthMiddleware) {
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)
}
g.GET("/user/:id", deps.UserPublic)

View File

@@ -36,6 +36,7 @@ type PostPageData struct {
Comments []CommentView
CommentsLocked bool
CanEdit bool
CanReportPost bool
}
// CommentView 评论
@@ -53,6 +54,7 @@ type CommentView struct {
LikeCount int
Liked bool
IsPrivate bool
CanReport bool
}
// PostView GET /post/:id
@@ -88,6 +90,7 @@ func (d Deps) PostView(c *gin.Context) {
CreatedLabel: formatTime(cm.CreatedAt),
Content: cm.Content, ContentHidden: cm.ContentHidden,
LikeCount: cm.LikeCount, Liked: cm.Liked, IsPrivate: cm.IsPrivate,
CanReport: ctx.IsSigned() && (cm.UserID == 0 || cm.UserID != ctx.UserID()),
}
if cm.ReplyTarget != nil {
view.ReplyToID = cm.ReplyTarget.ID
@@ -130,6 +133,7 @@ func (d Deps) PostView(c *gin.Context) {
BodyHTML: body, CommentCount: len(cv), Comments: cv,
CommentsLocked: post.CommentsLocked,
CanEdit: d.Post.CanUserEdit(post, ctx.UserID(), ctx.IsAdmin()),
CanReportPost: ctx.IsSigned() && post.UserID != ctx.UserID(),
})
}

72
routers/web/report.go Normal file
View File

@@ -0,0 +1,72 @@
package web
import (
"fmt"
"strconv"
"strings"
"github.com/gin-gonic/gin"
)
// PostReportPost 举报帖子
func (d Deps) PostReportPost(c *gin.Context) {
ctx := d.ctx(c)
postID, _ := strconv.ParseUint(c.Param("id"), 10, 64)
redir := fmt.Sprintf("/post/%d", postID)
if !ctx.CheckCSRF() {
ctx.SetFlash("无效请求,请重试")
ctx.Redirect(redir)
return
}
if d.Report == nil {
ctx.SetFlash("举报服务未就绪")
ctx.Redirect(redir)
return
}
if d.Limiter != nil && !d.Limiter.Allow("report", fmt.Sprintf("%d", ctx.UserID())) {
ctx.SetFlash("举报过于频繁,请稍后再试")
ctx.Redirect(redir)
return
}
reason := strings.TrimSpace(c.PostForm("reason"))
detail := strings.TrimSpace(c.PostForm("detail"))
if _, err := d.Report.Create(ctx.UserID(), uint(postID), reason, detail); err != nil {
ctx.SetFlash(err.Error())
ctx.Redirect(redir)
return
}
ctx.SetFlash("举报已提交,感谢反馈")
ctx.Redirect(redir)
}
// CommentReportPost 举报评论
func (d Deps) CommentReportPost(c *gin.Context) {
ctx := d.ctx(c)
postID, _ := strconv.ParseUint(c.Param("id"), 10, 64)
cid, _ := strconv.ParseUint(c.Param("cid"), 10, 64)
redir := fmt.Sprintf("/post/%d", postID)
if !ctx.CheckCSRF() {
ctx.SetFlash("无效请求,请重试")
ctx.Redirect(redir)
return
}
if d.Report == nil {
ctx.SetFlash("举报服务未就绪")
ctx.Redirect(redir)
return
}
if d.Limiter != nil && !d.Limiter.Allow("report", fmt.Sprintf("%d", ctx.UserID())) {
ctx.SetFlash("举报过于频繁,请稍后再试")
ctx.Redirect(redir)
return
}
reason := strings.TrimSpace(c.PostForm("reason"))
detail := strings.TrimSpace(c.PostForm("detail"))
if _, err := d.Report.CreateCommentReport(ctx.UserID(), uint(cid), reason, detail); err != nil {
ctx.SetFlash(err.Error())
ctx.Redirect(redir)
return
}
ctx.SetFlash("举报已提交,感谢反馈")
ctx.Redirect(redir)
}