Files
go_blog/handlers/admin_comment.go
T
kevin a0061b14e3 feat: 评论接口 JSON 化——/api/article/:slug/comments 与 admin 审核操作
- comment.go:commentForm 加 json tag;PostComment 校验分支改 APIError
  (404 article_not_found、403 comments_disabled/comments_guests_disabled、
  400 校验码、500 article_error),保留 guest 令牌与 flash 机制,成功返回
  {ok,redirect:/article/:slug#comment-N,comment_id}
- api.go:新增 APIErrorf(支持 %d/%s 占位符键如 comments_too_long)
- admin_comment.go:approve/reject/delete 改 JSON(parseUintParam 拒绝非
  数值 id 400),成功带原 ?saved=1&msg= 查询串 redirect
- main.go:PostComment 迁入 /api;评论审核三操作迁入 /api/admin/comments
- article.html:评论表单改 blogAPI 提交,错误内联 commentError div
- comment_list.html:审核操作改 to commentAct() 委托(confirm 在函数内,
  取消不发请求),成功 reload 保持筛选状态
- 测试:security_test env 路由同步 /api;session_upload 评论用例改 JSON
- main_test 冒烟补评论 API 路由断言;go build/vet/test 全绿
2026-08-27 19:36:09 +08:00

170 lines
4.7 KiB
Go

package handlers
import (
"net/http"
"strings"
"github.com/gin-gonic/gin"
"go_blog/models"
"gorm.io/gorm"
)
// adminCommentPageSize 限制后台每页显示的评论数量。
const adminCommentPageSize = 30
// commentListView 是 Comment 加上后台列表所需的派生展示字段。
type commentListView struct {
models.Comment
GravatarURL string
Initial string
AvatarColor string
MaskedEmail string
StatusLabel string
StatusBadge string
ArticleTitle string
ArticleSlug string
}
// CommentListPage 渲染后台评论审核列表,可通过
// ?status=pending|approved|rejected|all 按状态筛选。
func CommentListPage(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
tr := getTr(c)
status := strings.TrimSpace(c.Query("status"))
if status == "" {
status = "pending"
}
q := db.Model(&models.Comment{}).Order("created_at DESC")
switch status {
case "approved":
q = q.Where("status = ?", models.CommentApproved)
case "rejected":
q = q.Where("status = ?", models.CommentRejected)
case "all":
// 不过滤
default: // pending
status = "pending"
q = q.Where("status = ?", models.CommentPending)
}
var comments []models.Comment
q.Limit(adminCommentPageSize).Find(&comments)
// 用一次查询拉取引用的文章,避免 N+1。
ids := make(map[uint]struct{})
for _, cm := range comments {
ids[cm.ArticleID] = struct{}{}
}
articleMap := make(map[uint]models.Article)
if len(ids) > 0 {
idList := make([]uint, 0, len(ids))
for id := range ids {
idList = append(idList, id)
}
var articles []models.Article
db.Unscoped().Where("id IN ?", idList).Find(&articles)
for _, a := range articles {
articleMap[a.ID] = a
}
}
views := make([]commentListView, 0, len(comments))
// SECURITY_TODO #15:后台列表遵循平台开关——关闭时不发起 Gravatar
// 请求(改为使用前端占位头像)。
useGravatar := models.GetCommentConfig().UseGravatar
for _, cm := range comments {
gravURL := ""
if useGravatar {
gravURL = cm.GravatarURL(40)
}
v := commentListView{
Comment: cm,
GravatarURL: gravURL,
MaskedEmail: cm.MaskedEmail(),
Initial: cm.AuthorInitial(),
AvatarColor: avatarColorFor(cm.ID),
}
if a, ok := articleMap[cm.ArticleID]; ok {
v.ArticleTitle = a.Title
v.ArticleSlug = a.Slug
}
switch cm.Status {
case models.CommentPending:
v.StatusLabel = tr["comment_status_pending"]
v.StatusBadge = "bg-yellow-100 text-yellow-700"
case models.CommentApproved:
v.StatusLabel = tr["comment_status_approved"]
v.StatusBadge = "bg-green-100 text-green-700"
case models.CommentRejected:
v.StatusLabel = tr["comment_status_rejected"]
v.StatusBadge = "bg-red-100 text-red-700"
}
views = append(views, v)
}
// 标签徽章使用的待审数量。
var pendingCount int64
db.Model(&models.Comment{}).Where("status = ?", models.CommentPending).Count(&pendingCount)
data := DefaultData(c)
data["Title"] = tr["admin_comments_title"]
data["Comments"] = views
data["Status"] = status
data["PendingCount"] = pendingCount
if msg := c.Query("saved"); msg == "1" {
switch c.Query("msg") {
case "approved":
data["Success"] = tr["comment_approved"]
case "rejected":
data["Success"] = tr["comment_rejected"]
case "deleted":
data["Success"] = tr["comment_deleted"]
default:
data["Success"] = tr["settings_saved"]
}
}
c.HTML(http.StatusOK, "comment_list", data)
}
}
// CommentApprove 将评论标记为通过。
func CommentApprove(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
if id := parseUintParam(c, "id"); id == 0 {
APIError(c, http.StatusBadRequest, "api_invalid_request")
return
} else {
db.Model(&models.Comment{}).Where("id = ?", id).Update("status", models.CommentApproved)
}
APIOK(c, "/admin/comments?status=pending&saved=1&msg=approved", nil)
}
}
// CommentReject 将评论标记为拒绝(前端隐藏,后台列表保留)。
func CommentReject(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
if id := parseUintParam(c, "id"); id == 0 {
APIError(c, http.StatusBadRequest, "api_invalid_request")
return
} else {
db.Model(&models.Comment{}).Where("id = ?", id).Update("status", models.CommentRejected)
}
APIOK(c, "/admin/comments?status=pending&saved=1&msg=rejected", nil)
}
}
// CommentDelete 软删除一条评论。
func CommentDelete(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
if id := parseUintParam(c, "id"); id == 0 {
APIError(c, http.StatusBadRequest, "api_invalid_request")
return
} else {
db.Where("id = ?", id).Delete(&models.Comment{})
}
APIOK(c, "/admin/comments?status=all&saved=1&msg=deleted", nil)
}
}