package web import ( "fmt" "net/http" "strconv" "strings" "time" "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" ) // AdminChrome 后台布局公共字段 type AdminChrome struct { PageChrome NavActive string } func (d Deps) adminChrome(ctx *webctx.Context, title, nav string) AdminChrome { site := d.Settings.SiteBranding().Name if title == "" { title = "管理后台 · " + site } else { title = title + " · " + site } base := d.chrome(ctx, title, "", "") return AdminChrome{PageChrome: base, NavActive: nav} } type adminDashData struct { AdminChrome UserCount int64 PostCount int64 PendingPosts int64 PendingComments int64 BoardCount int64 } // AdminDashboard 概览 func (d Deps) AdminDashboard(c *gin.Context) { ctx := d.ctx(c) var users, posts, boards int64 _ = models.DB.Model(&models.User{}).Count(&users).Error _ = models.DB.Model(&models.Post{}).Where("status = ?", models.ContentStatusPublished).Count(&posts).Error _ = models.DB.Model(&models.Board{}).Count(&boards).Error pendingPosts, _ := d.Post.PendingPostCount() pendingComments, _ := d.Comment.PendingCommentCount() ctx.HTML(http.StatusOK, "admin/dashboard", adminDashData{ AdminChrome: d.adminChrome(ctx, "仪表盘", "dashboard"), UserCount: users, PostCount: posts, PendingPosts: pendingPosts, PendingComments: pendingComments, BoardCount: boards, }) } type adminBoardRow struct { ID uint Name string Description string Icon string ColorIndex int SortOrder int PostCount int } type adminBoardsData struct { AdminChrome Boards []adminBoardRow Form adminBoardForm } type adminBoardForm struct { ID uint Name string Description string Icon string ColorIndex int SortOrder int } // AdminBoardsGet 板块列表 func (d Deps) AdminBoardsGet(c *gin.Context) { ctx := d.ctx(c) d.renderAdminBoards(ctx, "", adminBoardForm{}) } func (d Deps) renderAdminBoards(ctx *webctx.Context, errMsg string, form adminBoardForm) { list, _ := d.Board.ListWithStats() rows := make([]adminBoardRow, 0, len(list)) for _, b := range list { rows = append(rows, adminBoardRow{ ID: b.ID, Name: b.Name, Description: b.Description, Icon: b.Icon, ColorIndex: b.ColorIndex, SortOrder: b.SortOrder, PostCount: b.PostCount, }) } data := adminBoardsData{ AdminChrome: d.adminChrome(ctx, "板块", "boards"), Boards: rows, Form: form, } data.Error = errMsg ctx.HTML(http.StatusOK, "admin/boards", data) } // AdminBoardCreate 新建板块 func (d Deps) AdminBoardCreate(c *gin.Context) { ctx := d.ctx(c) if !ctx.CheckCSRF() { d.renderAdminBoards(ctx, "无效请求,请重试", adminBoardFormFrom(c)) return } form := adminBoardFormFrom(c) if strings.TrimSpace(form.Name) == "" { d.renderAdminBoards(ctx, "请填写板块名称", form) return } if _, err := d.Board.Create(form.Name, form.Description, form.Icon, form.ColorIndex, form.SortOrder); err != nil { d.renderAdminBoards(ctx, err.Error(), form) return } ctx.SetFlash("板块已创建") ctx.Redirect("/admin/boards") } // AdminBoardUpdate 更新板块 func (d Deps) AdminBoardUpdate(c *gin.Context) { ctx := d.ctx(c) if !ctx.CheckCSRF() { d.renderAdminBoards(ctx, "无效请求,请重试", adminBoardFormFrom(c)) return } id, _ := strconv.ParseUint(c.Param("id"), 10, 64) form := adminBoardFormFrom(c) form.ID = uint(id) if strings.TrimSpace(form.Name) == "" { d.renderAdminBoards(ctx, "请填写板块名称", form) return } if err := d.Board.Update(uint(id), form.Name, form.Description, form.Icon, form.ColorIndex, form.SortOrder); err != nil { d.renderAdminBoards(ctx, err.Error(), form) return } ctx.SetFlash("板块已更新") ctx.Redirect("/admin/boards") } // AdminBoardDelete 删除板块 func (d Deps) AdminBoardDelete(c *gin.Context) { ctx := d.ctx(c) if !ctx.CheckCSRF() { ctx.SetFlash("无效请求,请重试") ctx.Redirect("/admin/boards") return } id, _ := strconv.ParseUint(c.Param("id"), 10, 64) if err := d.Board.Delete(uint(id)); err != nil { ctx.SetFlash(err.Error()) ctx.Redirect("/admin/boards") return } ctx.SetFlash("板块已删除") ctx.Redirect("/admin/boards") } func adminBoardFormFrom(c *gin.Context) adminBoardForm { color, _ := strconv.Atoi(c.PostForm("color_index")) sort, _ := strconv.Atoi(c.PostForm("sort_order")) return adminBoardForm{ Name: strings.TrimSpace(c.PostForm("name")), Description: strings.TrimSpace(c.PostForm("description")), Icon: strings.TrimSpace(c.PostForm("icon")), ColorIndex: color, SortOrder: sort, } } type adminModPostRow struct { ID uint Title string AuthorName string BoardName string CreatedAt string } type adminModCommentRow struct { ID uint PostID uint PostTitle string Floor int AuthorName string Excerpt string CreatedAt string } type adminModData struct { AdminChrome Posts []adminModPostRow Comments []adminModCommentRow } // AdminModerationGet 待审帖/评 func (d Deps) AdminModerationGet(c *gin.Context) { ctx := d.ctx(c) posts, _, _ := d.Post.List(services.PostListQuery{ Page: 1, Size: 50, ViewerIsAdmin: true, Status: models.ContentStatusPending, Sort: "latest", }) postRows := make([]adminModPostRow, 0, len(posts)) for _, p := range posts { author := "" if p.User.ID > 0 { author = p.User.Nickname if author == "" { author = p.User.Username } } board := "" if p.Board.ID > 0 { board = p.Board.Name } postRows = append(postRows, adminModPostRow{ ID: p.ID, Title: p.Title, AuthorName: author, BoardName: board, CreatedAt: p.CreatedAt.Format("2006-01-02 15:04"), }) } comments, _, _ := d.Comment.ListPending(1, 50) commentRows := make([]adminModCommentRow, 0, len(comments)) for _, cm := range comments { author := "游客" if cm.UserID > 0 { author = cm.User.Nickname if author == "" { author = cm.User.Username } } else if cm.GuestNick != "" { author = cm.GuestNick } excerpt := strings.TrimSpace(stripTagsRough(cm.Content)) runes := []rune(excerpt) if len(runes) > 80 { excerpt = string(runes[:80]) + "…" } title := "" if cm.Post.ID > 0 { title = cm.Post.Title } commentRows = append(commentRows, adminModCommentRow{ ID: cm.ID, PostID: cm.PostID, PostTitle: title, Floor: cm.Floor, AuthorName: author, Excerpt: excerpt, CreatedAt: cm.CreatedAt.Format("2006-01-02 15:04"), }) } ctx.HTML(http.StatusOK, "admin/moderation", adminModData{ AdminChrome: d.adminChrome(ctx, "内容审核", "moderation"), Posts: postRows, Comments: commentRows, }) } func stripTagsRough(s string) string { var b strings.Builder inTag := false for _, r := range s { switch { case r == '<': inTag = true case r == '>': inTag = false case !inTag: b.WriteRune(r) } } return b.String() } // AdminPostApprove 通过帖子 func (d Deps) AdminPostApprove(c *gin.Context) { ctx := d.ctx(c) if !ctx.CheckCSRF() { ctx.SetFlash("无效请求,请重试") ctx.Redirect("/admin/moderation") return } id, _ := strconv.ParseUint(c.Param("id"), 10, 64) if err := d.Post.SetStatus(uint(id), models.ContentStatusPublished); err != nil { ctx.SetFlash(err.Error()) } else { ctx.SetFlash("帖子已通过") } ctx.Redirect("/admin/moderation") } // AdminPostReject 拒绝帖子 func (d Deps) AdminPostReject(c *gin.Context) { ctx := d.ctx(c) if !ctx.CheckCSRF() { ctx.SetFlash("无效请求,请重试") ctx.Redirect("/admin/moderation") return } id, _ := strconv.ParseUint(c.Param("id"), 10, 64) reason := strings.TrimSpace(c.PostForm("reason")) if reason == "" { ctx.SetFlash("请填写拒绝原因") ctx.Redirect("/admin/moderation") return } post, err := d.Post.FindByID(uint(id)) if err != nil { ctx.SetFlash(err.Error()) ctx.Redirect("/admin/moderation") return } if err := d.Post.SetStatus(post.ID, models.ContentStatusRejected); err != nil { ctx.SetFlash(err.Error()) ctx.Redirect("/admin/moderation") return } pid := post.ID if d.Message != nil { _, _ = d.Message.SendSystem( post.UserID, "帖子《"+post.Title+"》未通过审核", services.FormatRejectContent(post.Title, post.ID, reason), models.MessageKindReject, &pid, nil, ) } ctx.SetFlash("已拒绝该帖") ctx.Redirect("/admin/moderation") } // AdminCommentApprove 通过评论 func (d Deps) AdminCommentApprove(c *gin.Context) { ctx := d.ctx(c) if !ctx.CheckCSRF() { ctx.SetFlash("无效请求,请重试") ctx.Redirect("/admin/moderation") return } id, _ := strconv.ParseUint(c.Param("id"), 10, 64) if err := d.Comment.SetStatus(uint(id), models.ContentStatusPublished); err != nil { ctx.SetFlash(err.Error()) } else { ctx.SetFlash("评论已通过") } ctx.Redirect("/admin/moderation") } // AdminCommentReject 拒绝评论 func (d Deps) AdminCommentReject(c *gin.Context) { ctx := d.ctx(c) if !ctx.CheckCSRF() { ctx.SetFlash("无效请求,请重试") ctx.Redirect("/admin/moderation") return } id, _ := strconv.ParseUint(c.Param("id"), 10, 64) reason := strings.TrimSpace(c.PostForm("reason")) if reason == "" { ctx.SetFlash("请填写拒绝原因") ctx.Redirect("/admin/moderation") return } cm, err := d.Comment.GetByID(uint(id)) if err != nil { ctx.SetFlash(err.Error()) ctx.Redirect("/admin/moderation") return } if err := d.Comment.SetStatus(cm.ID, models.ContentStatusRejected); err != nil { ctx.SetFlash(err.Error()) ctx.Redirect("/admin/moderation") return } if d.Message != nil && cm.UserID > 0 { pid := cm.PostID title := cm.Post.Title _, _ = d.Message.SendSystem( cm.UserID, fmt.Sprintf("评论未通过审核(帖 #%d)", cm.PostID), services.FormatCommentRejectContent(title, cm.PostID, cm.Floor, reason), models.MessageKindReject, &pid, nil, ) } ctx.SetFlash("已拒绝该评论") ctx.Redirect("/admin/moderation") } type adminSettingsData struct { AdminChrome Brand services.SiteBranding RatePost int RateComment int RateReg int RateLogin int RateWindow int FilterWords string FilterCount int } // AdminSettingsGet 设置页 func (d Deps) AdminSettingsGet(c *gin.Context) { ctx := d.ctx(c) d.renderAdminSettings(ctx, "") } func (d Deps) renderAdminSettings(ctx *webctx.Context, errMsg string) { words := d.Settings.FilterWordsContent() lim := d.Settings.Limits() data := adminSettingsData{ AdminChrome: d.adminChrome(ctx, "站点设置", "settings"), Brand: d.Settings.SiteBranding(), RatePost: lim.RateLimitPost, RateComment: lim.RateLimitComment, RateReg: lim.RateLimitRegister, RateLogin: lim.RateLimitLogin, RateWindow: lim.RateLimitWindowSec, FilterWords: words, FilterCount: services.CountFilterWords(words), } data.Error = errMsg ctx.HTML(http.StatusOK, "admin/settings", data) } // AdminSettingsBrandPost 品牌 func (d Deps) AdminSettingsBrandPost(c *gin.Context) { ctx := d.ctx(c) if !ctx.CheckCSRF() { d.renderAdminSettings(ctx, "无效请求,请重试") return } cur := d.Settings.SiteBranding() in := services.SiteBranding{ Name: strings.TrimSpace(c.PostForm("name")), Slogan: strings.TrimSpace(c.PostForm("slogan")), Description: strings.TrimSpace(c.PostForm("description")), Keywords: strings.TrimSpace(c.PostForm("keywords")), LogoMark: strings.TrimSpace(c.PostForm("logo_mark")), Logo: cur.Logo, Favicon: cur.Favicon, OGImage: cur.OGImage, ICPBeian: strings.TrimSpace(c.PostForm("icp_beian")), ICPBeianURL: strings.TrimSpace(c.PostForm("icp_beian_url")), FriendLinks: cur.FriendLinks, } if err := d.Settings.UpdateSiteBranding(in); err != nil { d.renderAdminSettings(ctx, err.Error()) return } ctx.SetFlash("品牌设置已保存") ctx.Redirect("/admin/settings") } // AdminSettingsLimitsPost 限流 func (d Deps) AdminSettingsLimitsPost(c *gin.Context) { ctx := d.ctx(c) if !ctx.CheckCSRF() { d.renderAdminSettings(ctx, "无效请求,请重试") return } postN, _ := strconv.Atoi(c.PostForm("rate_limit_post")) commentN, _ := strconv.Atoi(c.PostForm("rate_limit_comment")) regN, _ := strconv.Atoi(c.PostForm("rate_limit_register")) loginN, _ := strconv.Atoi(c.PostForm("rate_limit_login")) windowN, _ := strconv.Atoi(c.PostForm("rate_limit_window_sec")) if err := d.Settings.UpdateRateLimits(postN, commentN, regN, loginN, windowN); err != nil { d.renderAdminSettings(ctx, err.Error()) return } ctx.SetFlash("限流设置已保存") ctx.Redirect("/admin/settings") } // AdminSettingsFilterWordsPost 敏感词 func (d Deps) AdminSettingsFilterWordsPost(c *gin.Context) { ctx := d.ctx(c) if !ctx.CheckCSRF() { d.renderAdminSettings(ctx, "无效请求,请重试") return } content := c.PostForm("filter_words") if err := d.Settings.UpdateFilterWords(content, d.Filter); err != nil { d.renderAdminSettings(ctx, err.Error()) return } ctx.SetFlash(fmt.Sprintf("敏感词已更新(有效词 %d 个)· %s", services.CountFilterWords(content), time.Now().Format("15:04:05"))) ctx.Redirect("/admin/settings") }