package donat import ( "context" "donat-widget/internal/model" "donat-widget/internal/model/sql" "errors" "fmt" "github.com/georgysavva/scany/v2/pgxscan" "github.com/google/uuid" "github.com/jackc/pgx/v5" "log/slog" "strings" "time" ) func New(db model.Db) *RepoDonat { return &RepoDonat{ db: db, } } type RepoDonat struct { db model.Db } func (repoDonat *RepoDonat) CreateDonat( ctx context.Context, streamerID int, targetID *int, widgetID int, orderID uuid.UUID, amount int, text string, donatUser string, ) error { args := pgx.NamedArgs{ "streamer_id": streamerID, "widget_id": widgetID, "order_id": orderID, "target_id": targetID, "text": text, "amount": amount, "status": "pending", "donat_user": donatUser, } _, err := repoDonat.db.Insert(ctx, sql.CreateDonat, args) if err != nil { slog.Error(err.Error()) return err } return nil } func (repoDonat *RepoDonat) GetDonatsByStreamerID( ctx context.Context, streamerID int, page int, limit int, ) ([]*model.Donat, error) { offset := (page - 1) * limit args := pgx.NamedArgs{ "streamer_id": streamerID, "paid": true, "offset": offset, "page": page, } rows, err := repoDonat.db.Select(ctx, sql.GetDonatByStreamerID, args) if err != nil { slog.Error(err.Error()) return nil, err } defer rows.Close() defer rows.Close() var donats []*model.Donat err = pgxscan.ScanAll(&donats, rows) if err != nil { slog.Error(err.Error()) return nil, err } return donats, nil } func (repoDonat *RepoDonat) GetDonatByOrderID( ctx context.Context, orderID string, ) (*model.Donat, error) { args := pgx.NamedArgs{ "order_id": orderID, } rows, err := repoDonat.db.Select(ctx, sql.GetDonatByOrderID, args) if err != nil { slog.Error(err.Error()) return nil, err } defer rows.Close() defer rows.Close() var donats []*model.Donat err = pgxscan.ScanAll(&donats, rows) if err != nil { slog.Error(err.Error()) return nil, err } if len(donats) == 0 { return nil, errors.New("donat not found") } return donats[0], nil } func (repoDonat *RepoDonat) MarkDonatView( ctx context.Context, orderID string, ) error { args := pgx.NamedArgs{ "order_id": orderID, } err := repoDonat.db.Update(ctx, sql.MarkDonatView, args) if err != nil { slog.Error(err.Error()) return err } return nil } func (repoDonat *RepoDonat) MarkDonatPaid( ctx context.Context, orderID string, ) error { args := pgx.NamedArgs{ "order_id": orderID, } err := repoDonat.db.Update(ctx, sql.MarkDonatPaid, args) if err != nil { slog.Error(err.Error()) return err } return nil } func (repoDonat *RepoDonat) GetDonatPage( ctx context.Context, streamerID int, ) (model.DonatePage, error) { args := pgx.NamedArgs{ "streamer_id": streamerID, } rows, err := repoDonat.db.Select(ctx, sql.GetDonationPage, args) if err != nil { slog.Error(err.Error()) return model.DonatePage{}, err } defer rows.Close() defer rows.Close() var donatePage []*model.DonatePage err = pgxscan.ScanAll(&donatePage, rows) if err != nil { slog.Error(err.Error()) return model.DonatePage{}, err } if len(donatePage) == 0 { return model.DonatePage{}, errors.New("donat not found") } return *donatePage[0], nil } func (repoDonat *RepoDonat) GetDonatPageByLogin( ctx context.Context, streamerLogin string, ) (model.DonatePage, error) { args := pgx.NamedArgs{ "streamer_login": streamerLogin, } rows, err := repoDonat.db.Select(ctx, sql.GetDonationPageByLogin, args) if err != nil { slog.Error(err.Error()) return model.DonatePage{}, err } defer rows.Close() defer rows.Close() var donatePage []*model.DonatePage err = pgxscan.ScanAll(&donatePage, rows) if err != nil { slog.Error(err.Error()) return model.DonatePage{}, err } if len(donatePage) == 0 { return model.DonatePage{}, nil } return *donatePage[0], nil } func (repoDonat *RepoDonat) UpdateDonatePage( ctx context.Context, streamerID int, backgroundFileID *string, headImgFileID *string, avatarFileID *string, description *string, textAfterDonation *string, profileAvatar *bool, pageBackground *string, ) error { args := pgx.NamedArgs{ "streamer_id": streamerID, } if backgroundFileID != nil { args["background_img"] = *backgroundFileID } if headImgFileID != nil { args["head_img"] = *headImgFileID } if avatarFileID != nil { args["avatar"] = *avatarFileID } if description != nil { args["description"] = *description } if textAfterDonation != nil { args["text_after_donat"] = *textAfterDonation } if pageBackground != nil { args["page_background"] = *pageBackground } if profileAvatar != nil { args["profile_avatar"] = *profileAvatar } err := repoDonat.db.Exec(ctx, sql.UpdateDonatePage, args) if err != nil { slog.Error(err.Error()) return err } return nil } func (repoDonat *RepoDonat) GetLanguagesByStreamerID(ctx context.Context, streamerID int) ([]model.Language, error) { args := pgx.NamedArgs{ "streamer_id": streamerID, } // Выполняем SQL-запрос rows, err := repoDonat.db.Select(ctx, sql.GetLanguagesByStreamerID, args) if err != nil { slog.Error("Failed to execute query", "error", err) return nil, err } defer rows.Close() defer rows.Close() // Сканируем результаты в структуру Language var languages []*model.Language err = pgxscan.ScanAll(&languages, rows) if err != nil { slog.Error("Failed to scan rows", "error", err) return nil, err } // Проверяем ошибки, которые могли возникнуть при итерации по строкам if err := rows.Err(); err != nil { slog.Error("Error during rows iteration", "error", err) return nil, err } // Преобразуем []*model.Language в []model.Language result := make([]model.Language, len(languages)) for i, lang := range languages { result[i] = *lang } return result, nil } func (repoDonat *RepoDonat) GetVoiceSettingsByStreamerID( ctx context.Context, streamerID int, ) (model.VoiceSettingsResponse, error) { args := pgx.NamedArgs{ "streamer_id": streamerID, } rows, err := repoDonat.db.Select(ctx, sql.GetVoiceSettingsByStreamerID, args) if err != nil { slog.Error(err.Error()) return model.VoiceSettingsResponse{}, err } defer rows.Close() defer rows.Close() var voiceSettings []*model.VoiceSettingsResponse err = pgxscan.ScanAll(&voiceSettings, rows) if err != nil { slog.Error(err.Error()) return model.VoiceSettingsResponse{}, err } if len(voiceSettings) == 0 { return model.VoiceSettingsResponse{}, errors.New("voice settings not found") } return *voiceSettings[0], nil } func (repoDonat *RepoDonat) DeleteLanguagesByVoiceSettingID( ctx context.Context, voiceSettingID int, ) error { args := pgx.NamedArgs{ "voice_setting_id": voiceSettingID, } err := repoDonat.db.Exec(ctx, sql.DeleteLanguage, args) if err != nil { slog.Error("Failed to delete languages", "error", err) return err } return nil } func (repoDonat *RepoDonat) InsertLanguagesForVoiceSetting( ctx context.Context, voiceSettingID int, languageIDs []int, ) error { if len(languageIDs) == 0 { return nil // Нет языков для вставки } // Формируем список значений для вставки var valueStrings []string var valueArgs []interface{} for i, languageID := range languageIDs { valueStrings = append(valueStrings, fmt.Sprintf("($%d, $%d)", i*2+1, i*2+2)) valueArgs = append(valueArgs, voiceSettingID, languageID) } query := fmt.Sprintf(sql.InsertLanguagesForVoiceSetting, strings.Join(valueStrings, ",")) err := repoDonat.db.Exec(ctx, query, valueArgs...) if err != nil { slog.Error("Failed to insert languages", "error", err) return err } return nil } func (repoDonat *RepoDonat) GetLanguageIDsByISOCodes( ctx context.Context, isoCodes []string, ) ([]int, error) { args := pgx.NamedArgs{ "iso_codes": isoCodes, } rows, err := repoDonat.db.Select(ctx, sql.GetLangByISO, args) if err != nil { slog.Error("Failed to get language IDs by ISO codes", "error", err) return nil, err } defer rows.Close() defer rows.Close() var languageIDs []int for rows.Next() { var languageID int err := rows.Scan(&languageID) if err != nil { slog.Error("Failed to scan language ID", "error", err) return nil, err } languageIDs = append(languageIDs, languageID) } if err := rows.Err(); err != nil { slog.Error("Error during rows iteration", "error", err) return nil, err } return languageIDs, nil } func (repoDonat *RepoDonat) GetVoiceSettingIDByStreamerID( ctx context.Context, streamerID int, ) (int, error) { args := pgx.NamedArgs{ "streamer_id": streamerID, } var voiceSettingID int row := repoDonat.db.SelectOne(ctx, sql.VoiceIDByStreamer, args) err := row.Scan(&voiceSettingID) if err != nil { slog.Error(err.Error()) return 0, err } return voiceSettingID, nil } func (repoDonat *RepoDonat) UpdateVoiceSettings( ctx context.Context, streamerID int, settings model.UpdateVoiceSettings, ) error { args := pgx.NamedArgs{ "streamer_id": streamerID, } // Добавляем только те поля, которые не nil if settings.Enable != nil { args["enable"] = *settings.Enable } if settings.VoiceSpeed != nil { args["voice_speed"] = *settings.VoiceSpeed } if settings.Scenery != nil { args["scenery"] = *settings.Scenery } if settings.VoiceSoundPercent != nil { args["voice_sound_percent"] = *settings.VoiceSoundPercent } if settings.MinPrice != nil { args["min_price"] = *settings.MinPrice } // Выполняем SQL-запрос err := repoDonat.db.Update(ctx, sql.UpdateVoiceSettings, args) if err != nil { slog.Error("Failed to update voice settings", "error", err) return err } return nil } func (repoDonat *RepoDonat) GetFilterSettingsByStreamerID( ctx context.Context, streamerID int, ) (int, bool, error) { args := pgx.NamedArgs{ "streamer_id": streamerID, } row := repoDonat.db.SelectOne(ctx, sql.GetFilterSettings, args) var filterSettingID int var enableLinks bool err := row.Scan(&filterSettingID, &enableLinks) if err != nil { slog.Error("Failed to scan filter settings", "error", err) return 0, false, err } return filterSettingID, enableLinks, nil } func (repoDonat *RepoDonat) GetFilteredWords( ctx context.Context, filterId int, ) ([]string, error) { args := pgx.NamedArgs{ "donat_filter_id": filterId, } rows, err := repoDonat.db.Select(ctx, sql.GetFilterWords, args) if err != nil { slog.Error("Failed to get filter settings", "error", err) return nil, err } defer rows.Close() defer rows.Close() var filterWords []string err = pgxscan.ScanAll(&filterWords, rows) if err != nil { slog.Error("Failed to scan filter settings", "error", err) return nil, err } return filterWords, nil } func (repoDonat RepoDonat) UpdateFilterSettings( ctx context.Context, streamerID int, enableLinks *bool, ) error { if enableLinks != nil { args := pgx.NamedArgs{ "streamer_id": streamerID, "enable_links": enableLinks, } err := repoDonat.db.Exec(ctx, sql.UpdateFilterSettings, args) if err != nil { slog.Error("Failed to update filter settings", "error", err) return err } } return nil } func (repoDonat *RepoDonat) GetFilterIDByStreamer( ctx context.Context, streamerID int, ) (int, error) { args := pgx.NamedArgs{ "streamer_id": streamerID, } row := repoDonat.db.SelectOne(ctx, sql.GetFilterIdByStreamerID, args) var filterID int err := row.Scan(&filterID) if err != nil { slog.Error("Failed to scan filter settings", "error", err) return 0, err } return filterID, nil } func (repoDonat *RepoDonat) AddFilteredWords( ctx context.Context, filterID int, words []string, ) error { if len(words) == 0 { return nil } var valueStrings []string var valueArgs []interface{} for i, word := range words { valueStrings = append(valueStrings, fmt.Sprintf("($%d, $%d)", i*2+1, i*2+2)) valueArgs = append(valueArgs, filterID, word) } query := fmt.Sprintf(sql.InsertFilteredWordsBatch, strings.Join(valueStrings, ",")) err := repoDonat.db.Exec(ctx, query, valueArgs...) if err != nil { slog.Error("Failed to add filtered words", "error", err) return err } return nil } func (repoDonat *RepoDonat) RemoveFilteredWords( ctx context.Context, filterID int, words []string, ) error { if len(words) == 0 { return nil // Нет слов для удаления } args := pgx.NamedArgs{ "donat_filter_id": filterID, "words": words, } err := repoDonat.db.Exec(ctx, sql.DeleteFilteredWordsBatch, args) if err != nil { slog.Error("Failed to remove filtered words", "error", err) return err } return nil } func (repoDonat *RepoDonat) GetModeration(ctx context.Context, streamerID int) (model.ModerationResponse, error) { args := pgx.NamedArgs{ "streamer_id": streamerID, } row := repoDonat.db.SelectOne(ctx, sql.GetModeration, args) var response model.ModerationResponse err := row.Scan(&response.Enable, &response.Duration) if err != nil { slog.Error("Failed to get moderation state", "error", err) return model.ModerationResponse{}, err } return response, nil } func (repoDonat *RepoDonat) UpdateModeration( ctx context.Context, streamerID int, enable *bool, duration *int, ) error { args := pgx.NamedArgs{ "streamer_id": streamerID, } if enable != nil { args["enable"] = enable } if duration != nil { args["duration"] = duration } err := repoDonat.db.Exec(ctx, sql.UpdateModeration, args) if err != nil { slog.Error("Failed to update moderation state", "error", err) return err } return nil } func (repoDonat *RepoDonat) GetDonationsStats( ctx context.Context, streamerID int, period string, // "24h", "7d", "1m", "1y" ) ([]model.DonationStat, error) { var query string switch period { case "24h": query = sql.GetDonationsLast24Hours case "7d": query = sql.GetDonationsLast7Days case "1m": query = sql.GetDonationsLastMonth case "1y": query = sql.GetDonationsLastYear default: return nil, errors.New("invalid period") } args := pgx.NamedArgs{ "streamer_id": streamerID, } rows, err := repoDonat.db.Select(ctx, query, args) if err != nil { slog.Error(err.Error()) return nil, err } defer rows.Close() defer rows.Close() var stats []model.DonationStat err = pgxscan.ScanAll(&stats, rows) if err != nil { slog.Error(err.Error()) return nil, err } return stats, nil } func (repoDonat *RepoDonat) GetDonationsSummary( ctx context.Context, streamerID int, period string, // "24h", "7d", "1m", "1y" ) (model.DonationSummary, error) { var query string switch period { case "24h": query = sql.GetDonationsSummaryLast24Hours case "7d": query = sql.GetDonationsSummaryLast7Days case "1m": query = sql.GetDonationsSummaryLastMonth case "1y": query = sql.GetDonationsSummaryLastYear default: return model.DonationSummary{}, errors.New("invalid period") } args := pgx.NamedArgs{ "streamer_id": streamerID, } var summary model.DonationSummary row := repoDonat.db.SelectOne(ctx, query, args) err := row.Scan(&summary.TotalAmount, &summary.DonationsCount) if err != nil { slog.Error("Failed to get donations summary", "error", err) return model.DonationSummary{}, err } return summary, nil } func (repoDonat *RepoDonat) GetDonatModeration( ctx context.Context, streamerID int, ) (model.DonationModeration, error) { var donation model.DonationModeration args := pgx.NamedArgs{ "streamer_id": streamerID, } row := repoDonat.db.SelectOne(ctx, sql.GetLastModeration, args) if row == nil { return model.DonationModeration{}, nil } err := row.Scan( &donation.ID, &donation.TargetID, &donation.Status, &donation.Text, &donation.Amount, &donation.DonatUser, &donation.AcceptedTime, &donation.ShowName, &donation.ShowText, &donation.PlayContent, &donation.CreatedAt, &donation.UpdatedAt, ) if err != nil { slog.Error("Failed to get latest donation in moderation", "error", err) return model.DonationModeration{}, err } return donation, nil } func (repoDonat *RepoDonat) ModerateDonation( ctx context.Context, donatID int, streamerID int, updateModel model.ModerationDonat, ) error { args := pgx.NamedArgs{ "donat_id": donatID, "streamer_id": streamerID, "show_name": updateModel.ShowName, "show_text": updateModel.ShowText, "play_content": updateModel.PlayContent, } if updateModel.Accepted != nil { if *updateModel.Accepted { args["accepted_time"] = time.Now() args["status"] = "accepted_moderation" } else { args["rejected_time"] = time.Now() args["status"] = "rejected_moderation" } } err := repoDonat.db.Exec(ctx, sql.ModerateDonat, args) if err != nil { slog.Error("Failed to update donat", "error", err) return err } return nil } func (repoDonat *RepoDonat) InitNewStreamer( ctx context.Context, streamerID int, login string, defaultAvatar string, defaultBackground string, defaultHead string, ) error { donateArgs := pgx.NamedArgs{ "streamer_id": streamerID, "streamer_login": login, "avatar": defaultAvatar, "background_img": defaultBackground, "head_img": defaultHead, } if err := repoDonat.db.Exec(ctx, sql.InitDonatePage, donateArgs); err != nil { slog.Error("Failed to init donate page", "error", err) return fmt.Errorf("failed to create donate page: %w", err) } if err := repoDonat.db.Exec(ctx, sql.InitFilters, pgx.NamedArgs{"streamer_id": streamerID}); err != nil { slog.Error("Failed to init filters", "error", err) return fmt.Errorf("failed to create filters: %w", err) } if err := repoDonat.db.Exec(ctx, sql.InitModeration, pgx.NamedArgs{"streamer_id": streamerID}); err != nil { slog.Error("Failed to init moderation", "error", err) return fmt.Errorf("failed to create moderation: %w", err) } voiceArgs := pgx.NamedArgs{ "streamer_id": streamerID, "voice_speed": "medium", "voice_sound_percent": 80, "min_price": 100, } if err := repoDonat.db.Exec(ctx, sql.InitVoiceSettings, voiceArgs); err != nil { slog.Error("Failed to init voice settings", "error", err) return fmt.Errorf("failed to create voice settings: %w", err) } return nil } func (repoDonat *RepoDonat) GetPlayingDonat( ctx context.Context, streamerID int, ) (model.PlayingDonat, error) { args := pgx.NamedArgs{ "streamer_id": streamerID, } row := repoDonat.db.SelectOne(ctx, sql.GetPlayingDonats, args) if row == nil { return model.PlayingDonat{}, nil } var donatForPlaying model.PlayingDonat err := row.Scan( &donatForPlaying.Duration, &donatForPlaying.Image, &donatForPlaying.Audio, &donatForPlaying.Text, &donatForPlaying.Amount, &donatForPlaying.OrderID, &donatForPlaying.DonatUser, &donatForPlaying.PlayContent, &donatForPlaying.ShowName, &donatForPlaying.ShowText, ) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return model.PlayingDonat{}, nil } slog.Error("Failed to get playing donats", "error", err) return model.PlayingDonat{}, err } return donatForPlaying, nil } func (repoDonat *RepoDonat) UpdateStreamerLogin( ctx context.Context, streamerLogin string, streamerID int, ) error { args := pgx.NamedArgs{ "streamer_id": streamerID, "streamer_login": streamerLogin, } row := repoDonat.db.InsertReturningObj(ctx, sql.UpdateStreamerLogin, args) var updatedStreamerLogin string err := row.Scan(&updatedStreamerLogin) if err != nil { return err } if updatedStreamerLogin != streamerLogin { return errors.New("streamer login mismatch") } return nil }