package service import ( "context" "log" "time" "with_you/internal/model" "with_you/internal/pkg/hook" "with_you/internal/repository" "go.uber.org/zap" ) type UserProfileAuditService interface { SubmitAvatarAudit(ctx context.Context, userID, avatarURL string) (*model.UserProfileAudit, error) SubmitCoverAudit(ctx context.Context, userID, coverURL string) (*model.UserProfileAudit, error) SubmitBioAudit(ctx context.Context, userID, bio string) (*model.UserProfileAudit, error) GetPendingAudit(ctx context.Context, userID string, contentType model.UserProfileContentType) (*model.UserProfileAudit, error) GetAuditStatus(ctx context.Context, userID string) (*ProfileAuditStatusResponse, error) ModerateAudit(ctx context.Context, auditID string, status model.UserProfileAuditStatus, reason, reviewedBy string) error BatchModerateAudits(ctx context.Context, ids []string, status model.UserProfileAuditStatus, reason, reviewedBy string) error GetPendingList(ctx context.Context, contentType model.UserProfileContentType, page, pageSize int) ([]*model.UserProfileAudit, int64, error) GetPendingCounts(ctx context.Context) (int64, int64, int64, error) } type ProfileAuditStatusResponse struct { AvatarStatus string `json:"avatar_status"` CoverStatus string `json:"cover_status"` BioStatus string `json:"bio_status"` } type userProfileAuditServiceImpl struct { auditRepo repository.UserProfileAuditRepository userRepo repository.UserRepository hookManager *hook.Manager moderationHooks *hook.ModerationHooks } func NewUserProfileAuditService( auditRepo repository.UserProfileAuditRepository, userRepo repository.UserRepository, hookManager *hook.Manager, moderationHooks *hook.ModerationHooks, ) UserProfileAuditService { return &userProfileAuditServiceImpl{ auditRepo: auditRepo, userRepo: userRepo, hookManager: hookManager, moderationHooks: moderationHooks, } } func (s *userProfileAuditServiceImpl) SubmitAvatarAudit(ctx context.Context, userID, avatarURL string) (*model.UserProfileAudit, error) { audit := &model.UserProfileAudit{ UserID: userID, ContentType: model.UserProfileContentTypeAvatar, Content: avatarURL, Status: model.UserProfileAuditStatusPending, } if err := s.auditRepo.Create(audit); err != nil { return nil, err } go s.reviewAvatarAsync(audit.ID, userID, avatarURL) return audit, nil } func (s *userProfileAuditServiceImpl) SubmitCoverAudit(ctx context.Context, userID, coverURL string) (*model.UserProfileAudit, error) { audit := &model.UserProfileAudit{ UserID: userID, ContentType: model.UserProfileContentTypeCover, Content: coverURL, Status: model.UserProfileAuditStatusPending, } if err := s.auditRepo.Create(audit); err != nil { return nil, err } go s.reviewCoverAsync(audit.ID, userID, coverURL) return audit, nil } func (s *userProfileAuditServiceImpl) SubmitBioAudit(ctx context.Context, userID, bio string) (*model.UserProfileAudit, error) { audit := &model.UserProfileAudit{ UserID: userID, ContentType: model.UserProfileContentTypeBio, Content: bio, Status: model.UserProfileAuditStatusPending, } if err := s.auditRepo.Create(audit); err != nil { return nil, err } go s.reviewBioAsync(audit.ID, userID, bio) return audit, nil } func (s *userProfileAuditServiceImpl) GetPendingAudit(ctx context.Context, userID string, contentType model.UserProfileContentType) (*model.UserProfileAudit, error) { return s.auditRepo.GetPendingByUserID(userID, contentType) } func (s *userProfileAuditServiceImpl) GetAuditStatus(ctx context.Context, userID string) (*ProfileAuditStatusResponse, error) { resp := &ProfileAuditStatusResponse{ AvatarStatus: string(model.UserProfileAuditStatusApproved), CoverStatus: string(model.UserProfileAuditStatusApproved), BioStatus: string(model.UserProfileAuditStatusApproved), } if audit, err := s.auditRepo.GetByUserID(userID, model.UserProfileContentTypeAvatar); err == nil { resp.AvatarStatus = string(audit.Status) } if audit, err := s.auditRepo.GetByUserID(userID, model.UserProfileContentTypeCover); err == nil { resp.CoverStatus = string(audit.Status) } if audit, err := s.auditRepo.GetByUserID(userID, model.UserProfileContentTypeBio); err == nil { resp.BioStatus = string(audit.Status) } return resp, nil } func (s *userProfileAuditServiceImpl) ModerateAudit(ctx context.Context, auditID string, status model.UserProfileAuditStatus, reason, reviewedBy string) error { audit, err := s.auditRepo.GetByID(auditID) if err != nil { return err } now := time.Now() audit.Status = status audit.ReviewedAt = &now audit.ReviewedBy = reviewedBy audit.RejectReason = reason if err := s.auditRepo.Update(audit); err != nil { return err } if status == model.UserProfileAuditStatusApproved { if err := s.applyAuditContent(audit); err != nil { zap.L().Error("Failed to apply approved audit content", zap.String("audit_id", audit.ID), zap.String("user_id", audit.UserID), zap.Error(err), ) } } return nil } func (s *userProfileAuditServiceImpl) BatchModerateAudits(ctx context.Context, ids []string, status model.UserProfileAuditStatus, reason string, reviewedBy string) error { for _, id := range ids { if err := s.ModerateAudit(ctx, id, status, reason, reviewedBy); err != nil { zap.L().Warn("Failed to moderate audit in batch", zap.String("audit_id", id), zap.Error(err), ) } } return nil } func (s *userProfileAuditServiceImpl) GetPendingList(ctx context.Context, contentType model.UserProfileContentType, page, pageSize int) ([]*model.UserProfileAudit, int64, error) { return s.auditRepo.GetPendingList(contentType, page, pageSize) } func (s *userProfileAuditServiceImpl) GetPendingCounts(ctx context.Context) (int64, int64, int64, error) { return s.auditRepo.CountPendingAll() } func (s *userProfileAuditServiceImpl) reviewAvatarAsync(auditID, userID, avatarURL string) { defer func() { if r := recover(); r != nil { log.Printf("[ERROR] Panic in avatar moderation async flow, audit=%s panic=%v", auditID, r) } }() result := &hook.ModerationResult{} if s.moderationHooks != nil && s.hookManager != nil { result = s.moderationHooks.ModerateAvatar(context.Background(), s.hookManager, &hook.UserAvatarModerateHookData{ UserID: userID, AvatarURL: avatarURL, }) } else { result.Approved = true result.ReviewedBy = "system" } audit, err := s.auditRepo.GetByID(auditID) if err != nil { zap.L().Error("Failed to get audit for avatar moderation result", zap.String("audit_id", auditID), zap.Error(err), ) return } now := time.Now() if !result.Approved { if result.NeedsReview { audit.ReviewedBy = "ai" zap.L().Info("User avatar flagged for manual review", zap.String("audit_id", auditID), zap.String("user_id", userID), ) } else { audit.Status = model.UserProfileAuditStatusRejected audit.RejectReason = result.RejectReason audit.ReviewedBy = result.ReviewedBy audit.ReviewedAt = &now zap.L().Info("User avatar rejected by moderation", zap.String("audit_id", auditID), zap.String("user_id", userID), zap.String("reason", result.RejectReason), ) } } else { audit.Status = model.UserProfileAuditStatusApproved audit.ReviewedBy = result.ReviewedBy audit.ReviewedAt = &now if err := s.applyAuditContent(audit); err != nil { zap.L().Error("Failed to apply approved avatar", zap.String("audit_id", auditID), zap.Error(err), ) } } if err := s.auditRepo.Update(audit); err != nil { zap.L().Error("Failed to update avatar audit status", zap.String("audit_id", auditID), zap.Error(err), ) } } func (s *userProfileAuditServiceImpl) reviewCoverAsync(auditID, userID, coverURL string) { defer func() { if r := recover(); r != nil { log.Printf("[ERROR] Panic in cover moderation async flow, audit=%s panic=%v", auditID, r) } }() result := &hook.ModerationResult{} if s.moderationHooks != nil && s.hookManager != nil { result = s.moderationHooks.ModerateCover(context.Background(), s.hookManager, &hook.UserCoverModerateHookData{ UserID: userID, CoverURL: coverURL, }) } else { result.Approved = true result.ReviewedBy = "system" } audit, err := s.auditRepo.GetByID(auditID) if err != nil { zap.L().Error("Failed to get audit for cover moderation result", zap.String("audit_id", auditID), zap.Error(err), ) return } now := time.Now() if !result.Approved { if result.NeedsReview { audit.ReviewedBy = "ai" zap.L().Info("User cover flagged for manual review", zap.String("audit_id", auditID), zap.String("user_id", userID), ) } else { audit.Status = model.UserProfileAuditStatusRejected audit.RejectReason = result.RejectReason audit.ReviewedBy = result.ReviewedBy audit.ReviewedAt = &now zap.L().Info("User cover rejected by moderation", zap.String("audit_id", auditID), zap.String("user_id", userID), zap.String("reason", result.RejectReason), ) } } else { audit.Status = model.UserProfileAuditStatusApproved audit.ReviewedBy = result.ReviewedBy audit.ReviewedAt = &now if err := s.applyAuditContent(audit); err != nil { zap.L().Error("Failed to apply approved cover", zap.String("audit_id", auditID), zap.Error(err), ) } } if err := s.auditRepo.Update(audit); err != nil { zap.L().Error("Failed to update cover audit status", zap.String("audit_id", auditID), zap.Error(err), ) } } func (s *userProfileAuditServiceImpl) reviewBioAsync(auditID, userID, bio string) { defer func() { if r := recover(); r != nil { log.Printf("[ERROR] Panic in bio moderation async flow, audit=%s panic=%v", auditID, r) } }() result := &hook.ModerationResult{} if s.moderationHooks != nil && s.hookManager != nil { result = s.moderationHooks.ModerateBio(context.Background(), s.hookManager, &hook.UserBioModerateHookData{ UserID: userID, Bio: bio, }) } else { result.Approved = true result.ReviewedBy = "system" } audit, err := s.auditRepo.GetByID(auditID) if err != nil { zap.L().Error("Failed to get audit for bio moderation result", zap.String("audit_id", auditID), zap.Error(err), ) return } now := time.Now() if !result.Approved { if result.NeedsReview { audit.ReviewedBy = "ai" zap.L().Info("User bio flagged for manual review", zap.String("audit_id", auditID), zap.String("user_id", userID), ) } else { audit.Status = model.UserProfileAuditStatusRejected audit.RejectReason = result.RejectReason audit.ReviewedBy = result.ReviewedBy audit.ReviewedAt = &now zap.L().Info("User bio rejected by moderation", zap.String("audit_id", auditID), zap.String("user_id", userID), zap.String("reason", result.RejectReason), ) } } else { audit.Status = model.UserProfileAuditStatusApproved audit.ReviewedBy = result.ReviewedBy audit.ReviewedAt = &now if err := s.applyAuditContent(audit); err != nil { zap.L().Error("Failed to apply approved bio", zap.String("audit_id", auditID), zap.Error(err), ) } } if err := s.auditRepo.Update(audit); err != nil { zap.L().Error("Failed to update bio audit status", zap.String("audit_id", auditID), zap.Error(err), ) } } func (s *userProfileAuditServiceImpl) applyAuditContent(audit *model.UserProfileAudit) error { user, err := s.userRepo.GetByID(audit.UserID) if err != nil { return err } switch audit.ContentType { case model.UserProfileContentTypeAvatar: user.Avatar = audit.Content case model.UserProfileContentTypeCover: user.CoverURL = audit.Content case model.UserProfileContentTypeBio: user.Bio = audit.Content default: return nil } return s.userRepo.Update(user) }