v0.1 - макет интерфейса, аутентификация через логин, аккаунт админа, создание партии в 2 этапа, базовые настройки профиля и группы, переключение между группами, статистика

This commit is contained in:
2026-06-16 17:41:06 +03:00
parent 6ab74f01aa
commit 56b5d09a4d
123 changed files with 13572 additions and 21 deletions
+406
View File
@@ -0,0 +1,406 @@
"""Pydantic-схемы (граница HTTP). Из них генерируется OpenAPI → типы фронта."""
from __future__ import annotations
from datetime import date
from typing import Any, Literal
from pydantic import BaseModel, ConfigDict, Field
WinReason = Literal["objectives", "worlds", "plastic", "resources"]
# ─── Auth ────────────────────────────────────────────────────────────────────
class AuthConfig(BaseModel):
# Доступные методы входа: ["telegram"] в проде, ["telegram","stub"] в деве.
methods: list[str] = []
telegram_bot_username: str | None = None
class TelegramAuthPayload(BaseModel):
# Полезная нагрузка Telegram Login Widget (проверяется по HMAC).
model_config = ConfigDict(extra="allow")
id: int
auth_date: int
hash: str
first_name: str | None = None
last_name: str | None = None
username: str | None = None
photo_url: str | None = None
class DevLogin(BaseModel):
nickname: str
class DevUserCreate(BaseModel):
nickname: str
class DevUserRead(BaseModel):
id: int
nickname: str
is_active: bool = True
class OkResponse(BaseModel):
ok: bool = True
# ─── Справочники ─────────────────────────────────────────────────────────────
class ExpansionRead(BaseModel):
id: int
code: str
name_ru: str
is_base: bool
class FactionRead(BaseModel):
id: int
code: str
name_ru: str
expansion_id: int
class FactionRename(BaseModel):
name_ru: str
# ─── Пользователь ────────────────────────────────────────────────────────────
class GroupBrief(BaseModel):
id: int
name: str
role: str
class UserRead(BaseModel):
id: int
nickname: str
role: str
auth_provider: str
telegram_id: int | None = None
active_group_id: int | None = None
class MeRead(UserRead):
groups: list[GroupBrief] = []
class NicknameUpdate(BaseModel):
nickname: str
class ActiveGroupUpdate(BaseModel):
group_id: int | None = None
class NicknameAvailable(BaseModel):
available: bool
# ─── Группы и членство ───────────────────────────────────────────────────────
class GroupCreate(BaseModel):
name: str
expansion_ids: list[int] = []
class GroupRename(BaseModel):
name: str
class GroupExpansionsUpdate(BaseModel):
expansion_ids: list[int] = []
class GroupDetail(BaseModel):
id: int
name: str
owner_id: int
my_role: str
expansion_ids: list[int] = []
class MemberRead(BaseModel):
user_id: int
nickname: str
role: str
class MemberAdd(BaseModel):
nickname: str
class MemberRoleUpdate(BaseModel):
role: str
# ─── Партии ──────────────────────────────────────────────────────────────────
class RandomizeRequest(BaseModel):
group_id: int
exclude_faction_ids: list[int] = []
class RandomizeResponse(BaseModel):
faction: FactionRead
# Этап 1 (старт): только ростер, без мест.
class RosterParticipant(BaseModel):
user_id: int
faction_id: int
was_random: bool = False
class MatchCreate(BaseModel):
group_id: int
participants: list[RosterParticipant]
# Этап 2 (завершение): места, комментарии, причина победы.
class MatchFinishParticipant(BaseModel):
user_id: int
place: int = Field(ge=1)
comment: str | None = None
faction_id: int | None = None # опц. смена фракции при завершении
class MatchFinish(BaseModel):
participants: list[MatchFinishParticipant]
win_reason: WinReason
overall_comment: str | None = None
# Полный участник (правка завершённой партии админом).
class ParticipantInput(BaseModel):
user_id: int
faction_id: int
place: int = Field(ge=1)
was_random: bool = False
comment: str | None = None
class MatchUpdate(BaseModel):
played_at: date | None = None
overall_comment: str | None = None
win_reason: WinReason | None = None
participants: list[ParticipantInput] | None = None
class MatchParticipantRead(BaseModel):
user_id: int
nickname: str
faction_id: int
faction_name: str
place: int | None = None
was_random: bool
comment: str | None = None
class MatchRead(BaseModel):
id: int
group_id: int
status: str
played_at: date
started_at: str | None = None
finished_at: str | None = None
duration_minutes: int | None = None
win_reason: WinReason | None = None
player_count: int
overall_comment: str | None = None
created_by: int
can_modify: bool = False # может ли текущий зритель править/завершать партию
participants: list[MatchParticipantRead] = []
attachments: list[Any] = [] # задел под вложения (всегда пусто в v1)
# ─── Статистика ──────────────────────────────────────────────────────────────
class OverallStats(BaseModel):
games: int
wins: int
win_rate: float
avg_place: float | None = None
score: float | None = None
class LeaderboardEntry(OverallStats):
user_id: int
nickname: str
rank: int | None = None
class Leaderboard(BaseModel):
entries: list[LeaderboardEntry] = []
provisional: list[LeaderboardEntry] = []
min_games: int
class FactionStat(BaseModel):
faction_id: int
code: str
name_ru: str
expansion_code: str
games: int
wins: int
win_rate: float
avg_place: float | None = None
score: float | None = None
class RecentFormItem(BaseModel):
place: int
player_count: int
played_at: str
class ProfileStats(BaseModel):
user_id: int
overall: OverallStats
factions: list[FactionStat] = []
best_faction: FactionStat | None = None
worst_faction: FactionStat | None = None
most_played_faction: FactionStat | None = None
recent_form: list[RecentFormItem] = []
class FactionMeta(BaseModel):
faction_id: int
code: str
name_ru: str
games: int
wins: int
available: bool
class GroupStats(BaseModel):
group_id: int
total_matches: int
last_match_at: str | None = None
leaderboard: list[LeaderboardEntry] = []
provisional: list[LeaderboardEntry] = []
faction_meta: list[FactionMeta] = []
min_games: int
class MatchListParticipant(BaseModel):
user_id: int
nickname: str
faction_id: int
faction_name: str
place: int | None = None
was_random: bool
comment: str | None = None
class MatchListItem(BaseModel):
id: int
status: str
played_at: str
started_at: str | None = None
finished_at: str | None = None
duration_minutes: int | None = None
win_reason: WinReason | None = None
player_count: int
overall_comment: str | None = None
created_by: int
participants: list[MatchListParticipant] = []
class MatchList(BaseModel):
items: list[MatchListItem] = []
total: int
limit: int
offset: int
class GroupBriefStats(OverallStats):
id: int
name: str
class HomeInProgressMatch(BaseModel):
id: int
group_id: int
group_name: str
started_at: str | None = None
player_count: int
participants: list[MatchListParticipant] = []
class HomeResponse(BaseModel):
leaderboard: list[LeaderboardEntry] = []
provisional: list[LeaderboardEntry] = []
profile: ProfileStats
active_group: GroupBriefStats | None = None
in_progress: list[HomeInProgressMatch] = []
min_games: int
# ─── Админ ───────────────────────────────────────────────────────────────────
class AdminLogin(BaseModel):
username: str
password: str
class AdminMe(BaseModel):
id: int
nickname: str
role: str
class AdminUserRead(BaseModel):
id: int
nickname: str
role: str
is_active: bool
auth_provider: str
telegram_id: int | None = None
created_at: str
class AdminUserUpdate(BaseModel):
nickname: str | None = None
is_active: bool | None = None
class AdminGroupRead(BaseModel):
id: int
name: str
owner_id: int
created_at: str
class AdminMatchRead(BaseModel):
id: int
group_id: int
group_name: str | None = None
status: str
played_at: str
duration_minutes: int | None = None
win_reason: WinReason | None = None
player_count: int
created_by: int
created_at: str
class AuditLogItem(BaseModel):
id: int
actor_id: int | None = None
action: str
entity_type: str
entity_id: int | None = None
payload: dict | None = None
ip: str | None = None
user_agent: str | None = None
created_at: str
class AuditLogList(BaseModel):
items: list[AuditLogItem] = []
limit: int
offset: int