Перейти к содержанию

Заявки на сборки (user_builds)

Юзеры могут подать свою собственную сборку на review админу. Это собранный набор «redux X + gunpack Y + armor Z + такие настройки» который юзер хочет опубликовать как «билд», чтобы другие могли применить одной кнопкой. Админ модерирует.

Зачем нужно

Хантера часто просят «дайте билд топ-стримеров». Юзер делает свой mix, ему нравится, он хочет поделиться. Мы превращаем это в user_builds row + публичный HNT-код, который другие юзеры применяют одной кнопкой.

Структура

create table user_builds (
  id              uuid primary key,
  name            text not null,
  description     text,
  cover_url       text,
  author_user_id  text not null,    -- кто создал
  author_username text,
  hnt_code        text unique,      -- short-link, см. HNT-коды
  payload         jsonb not null,   -- сам "snapshot" — то же что в HNT
  status          text default 'pending',  -- pending / approved / rejected
  review_notes    text,             -- что админ написал юзеру
  popularity      int default 0,
  created_at      timestamptz default now(),
  is_deleted      bool default false
);

payload — это тот же HntPayloadDto который мы храним в hnt_codes, но привязан к постоянному билду (не personal-code на одного юзера, а публикуемая сборка с именем и описанием).

Workflow

flowchart TD
  User[Юзер: 'Опубликовать сборку'] --> Form[Заполняет: name, description, cover]
  Form --> Capture[Lаунчер собирает payload
через CaptureHntPayloadAsync — то же что для HNT] Capture --> Insert[INSERT user_builds row
status='pending'
hnt_code = NULL пока] Insert --> AdminReview[Админ в '/admin/build-requests' видит pending] AdminReview --> Approve{Решение} Approve -->|approve| Generate[generate hnt_code
status='approved'] Approve -->|reject| Reject[status='rejected'
review_notes='почему'] Generate --> Visible[Билд видим в Browse → User Builds]

Handlers

HunterGraphics.Shell/Bridge/WebViewBridgeHost.cs
["userBuildSubmit"]      // юзер сабмитит свой билд
["userBuildsList"]       // публичный list approved
["userBuildGetByHntCode"] // открыть билд по hnt-коду
["adminBuildList"]       // админ видит все включая pending
["adminBuildApprove"]    // approve + generate hnt_code
["adminBuildReject"]     // reject с reason
["adminBuildDelete"]     // soft-delete

userBuildSubmit:

public async Task<UserBuildDto> UserBuildSubmitAsync(UserBuildDraftDto draft)
{
    if (string.IsNullOrWhiteSpace(draft.AuthorUserId))
        throw new InvalidOperationException("Только авторизованные юзеры могут submit'ить билды");

    // Capture текущий state — то же что для HNT
    var payload = await CaptureHntPayloadAsync();

    // Insert как pending
    var id = Guid.NewGuid().ToString();
    var coverUrl = draft.CoverBytes is null ? null
        : await _r2.UploadAsync($"user-builds/{id}/cover.png", draft.CoverBytes);

    var row = await _userBuildsRepo.InsertAsync(new UserBuildRow {
        Id = id, Name = draft.Name, Description = draft.Description,
        CoverUrl = coverUrl, AuthorUserId = draft.AuthorUserId,
        Payload = JsonSerializer.SerializeToElement(payload, _hntJsonOpts),
        Status = "pending",
    });

    return ToUserBuildDto(row);
}

Approve в админке

public async Task<UserBuildDto> AdminBuildApproveAsync(string buildId)
{
    var row = await _userBuildsRepo.GetAsync(buildId);
    if (row.Status != "pending")
        throw new InvalidOperationException("Build уже processed");

    // Generate hnt_code (short token, 8 chars, unique)
    var code = await _hntCodesRepo.GenerateUniqueCodeAsync();

    var updated = await _userBuildsRepo.ApproveAsync(buildId, code);
    return ToUserBuildDto(updated);
}

hnt_code пишется в обе таблицы — user_builds.hnt_code (для быстрого lookup'а билда) и hnt_codes row (так что код работает через стандартный HNT flow тоже). Юзер может либо открыть билд в каталоге, либо вбить код в HNT-modal — оба пути приведут к тому же payload'у.

Reject

public async Task<UserBuildDto> AdminBuildRejectAsync(string buildId, string notes)
{
    var updated = await _userBuildsRepo.RejectAsync(buildId, notes);
    return ToUserBuildDto(updated);
}

Юзер видит свой rejected билд в «Мои билды» с notes от админа: «не подходит — содержит чужой контент без credit'а». Может удалить или пересоздать.

Что админ модерирует

  • Имя/описание не оскорбительное (билды публичные, попадают в каталог).
  • Cover не нарушает копирайт (часто юзеры суют арт без разрешения).
  • Билд работает. Админ может фактически применить payload себе и проверить.
  • Не дубль. Если такой же билд уже опубликован — reject с notes «дубль X».

Popularity

popularity инкремент'ится при каждом install. Юзеры в каталоге могут сортировать по популярности. Раз в день батч-job (Supabase scheduled function) приводит popularity к decay'у — старые билды теряют вес, новые имеют шанс попасть в топ.

Дальше: PRO Players →