feat(moderation): add user profile content moderation for avatars, covers, and bios
All checks were successful
Build Backend / build (push) Successful in 4m24s
Build Backend / build-docker (push) Successful in 1m14s

This commit is contained in:
lafay
2026-04-15 11:50:47 +08:00
parent b6f2df87c4
commit 90495385cd
23 changed files with 1561 additions and 55 deletions

View File

@@ -0,0 +1,129 @@
package handler
import (
"strconv"
"carrot_bbs/internal/model"
"carrot_bbs/internal/pkg/response"
"carrot_bbs/internal/service"
"github.com/gin-gonic/gin"
)
type AdminProfileAuditHandler struct {
profileAuditService service.UserProfileAuditService
}
func NewAdminProfileAuditHandler(profileAuditService service.UserProfileAuditService) *AdminProfileAuditHandler {
return &AdminProfileAuditHandler{
profileAuditService: profileAuditService,
}
}
func (h *AdminProfileAuditHandler) GetPendingList(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
if page <= 0 {
page = 1
}
if pageSize <= 0 || pageSize > 100 {
pageSize = 20
}
contentType := c.Query("content_type")
var ct model.UserProfileContentType
switch contentType {
case "avatar":
ct = model.UserProfileContentTypeAvatar
case "cover":
ct = model.UserProfileContentTypeCover
case "bio":
ct = model.UserProfileContentTypeBio
default:
ct = ""
}
audits, total, err := h.profileAuditService.GetPendingList(c.Request.Context(), ct, page, pageSize)
if err != nil {
response.InternalServerError(c, "failed to get pending profile audits")
return
}
items := make([]map[string]any, 0, len(audits))
for _, audit := range audits {
item := map[string]any{
"id": audit.ID,
"user_id": audit.UserID,
"content_type": audit.ContentType,
"content": audit.Content,
"status": audit.Status,
"created_at": audit.CreatedAt.Format("2006-01-02T15:04:05Z07:00"),
}
if audit.User != nil {
item["user"] = map[string]any{
"id": audit.User.ID,
"username": audit.User.Username,
"nickname": audit.User.Nickname,
"avatar": audit.User.Avatar,
}
}
items = append(items, item)
}
response.Paginated(c, items, total, page, pageSize)
}
func (h *AdminProfileAuditHandler) ModerateAudit(c *gin.Context) {
auditID := c.Param("id")
if auditID == "" {
response.BadRequest(c, "audit id is required")
return
}
var req struct {
Status string `json:"status" binding:"required"`
Reason string `json:"reason"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
if req.Status != string(model.UserProfileAuditStatusApproved) && req.Status != string(model.UserProfileAuditStatusRejected) {
response.BadRequest(c, "invalid status, must be 'approved' or 'rejected'")
return
}
reviewedBy := c.GetString("user_id")
if err := h.profileAuditService.ModerateAudit(c.Request.Context(), auditID, model.UserProfileAuditStatus(req.Status), req.Reason, reviewedBy); err != nil {
response.HandleError(c, err, "failed to moderate profile audit")
return
}
response.Success(c, map[string]any{"success": true})
}
func (h *AdminProfileAuditHandler) BatchModerate(c *gin.Context) {
var req struct {
IDs []string `json:"ids" binding:"required"`
Status string `json:"status" binding:"required"`
Reason string `json:"reason"`
}
if err := c.ShouldBindJSON(&req); err != nil {
response.BadRequest(c, err.Error())
return
}
if req.Status != string(model.UserProfileAuditStatusApproved) && req.Status != string(model.UserProfileAuditStatusRejected) {
response.BadRequest(c, "invalid status, must be 'approved' or 'rejected'")
return
}
reviewedBy := c.GetString("user_id")
if err := h.profileAuditService.BatchModerateAudits(c.Request.Context(), req.IDs, model.UserProfileAuditStatus(req.Status), req.Reason, reviewedBy); err != nil {
response.InternalServerError(c, "failed to batch moderate profile audits")
return
}
response.Success(c, map[string]any{"success": true})
}

View File

@@ -32,13 +32,13 @@ func normalizePagination(page, pageSize int) (int, int) {
// UserHandler 用户处理器
type UserHandler struct {
userService service.UserService
activityService service.UserActivityService // 用户活跃统计服务
jwtService *service.JWTService
logService *service.LogService
userService service.UserService
activityService service.UserActivityService
profileAuditService service.UserProfileAuditService
jwtService *service.JWTService
logService *service.LogService
}
// NewUserHandler 创建用户处理器
func NewUserHandler(userService service.UserService, activityService service.UserActivityService, logService *service.LogService) *UserHandler {
return &UserHandler{
userService: userService,
@@ -47,6 +47,10 @@ func NewUserHandler(userService service.UserService, activityService service.Use
}
}
func (h *UserHandler) SetProfileAuditService(profileAuditService service.UserProfileAuditService) {
h.profileAuditService = profileAuditService
}
// SetJWTService 设置JWT服务
func (h *UserHandler) SetJWTService(jwtService *service.JWTService) {
h.jwtService = jwtService
@@ -318,6 +322,7 @@ func (h *UserHandler) UpdateUser(c *gin.Context) {
Website string `json:"website"`
Location string `json:"location"`
Avatar string `json:"avatar"`
CoverURL string `json:"cover_url"`
Phone *string `json:"phone"`
Email *string `json:"email"`
}
@@ -337,18 +342,12 @@ func (h *UserHandler) UpdateUser(c *gin.Context) {
if req.Nickname != "" {
user.Nickname = req.Nickname
}
if req.Bio != "" {
user.Bio = req.Bio
}
if req.Website != "" {
user.Website = req.Website
}
if req.Location != "" {
user.Location = req.Location
}
if req.Avatar != "" {
user.Avatar = req.Avatar
}
if req.Phone != nil {
user.Phone = req.Phone
}
@@ -359,20 +358,53 @@ func (h *UserHandler) UpdateUser(c *gin.Context) {
user.Email = req.Email
}
avatarNeedsAudit := req.Avatar != "" && req.Avatar != user.Avatar && h.profileAuditService != nil
coverNeedsAudit := req.CoverURL != "" && req.CoverURL != user.CoverURL && h.profileAuditService != nil
bioNeedsAudit := req.Bio != "" && req.Bio != user.Bio && h.profileAuditService != nil
if avatarNeedsAudit {
if _, err := h.profileAuditService.SubmitAvatarAudit(c.Request.Context(), userID, req.Avatar); err != nil {
avatarNeedsAudit = false
user.Avatar = req.Avatar
}
} else if req.Avatar != "" {
user.Avatar = req.Avatar
}
if coverNeedsAudit {
if _, err := h.profileAuditService.SubmitCoverAudit(c.Request.Context(), userID, req.CoverURL); err != nil {
coverNeedsAudit = false
user.CoverURL = req.CoverURL
}
} else if req.CoverURL != "" {
user.CoverURL = req.CoverURL
}
if bioNeedsAudit {
if _, err := h.profileAuditService.SubmitBioAudit(c.Request.Context(), userID, req.Bio); err != nil {
bioNeedsAudit = false
user.Bio = req.Bio
}
} else if req.Bio != "" {
user.Bio = req.Bio
}
err = h.userService.UpdateUser(c.Request.Context(), user)
if err != nil {
response.InternalServerError(c, "failed to update user")
return
}
// 实时计算帖子数量
postsCount, err := h.userService.GetUserPostCount(c.Request.Context(), userID)
if err != nil {
// 如果获取失败,使用数据库中的值
postsCount = int64(user.PostsCount)
}
response.Success(c, dto.ConvertUserToDetailResponseWithPostsCount(user, int(postsCount)))
resp := gin.H{"user": dto.ConvertUserToDetailResponseWithPostsCount(user, int(postsCount))}
if avatarNeedsAudit || coverNeedsAudit || bioNeedsAudit {
resp["audit_pending"] = true
}
response.Success(c, resp)
}
// SendEmailVerifyCode 发送当前用户邮箱验证码