Заявки на сборки (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¶
["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'у — старые билды теряют вес, новые имеют шанс попасть в топ.