generated from VLADIMIR/template
fix
This commit is contained in:
@@ -1,12 +1,9 @@
|
||||
// Package repos contains database repositories and persistence models.
|
||||
package repos
|
||||
|
||||
// TeamAnswer — ответ команды на вопрос дела (вопросы живут в сценарии,
|
||||
// ответы — в таблице team_answers).
|
||||
type TeamAnswer struct {
|
||||
TeamID int
|
||||
QuestionCode string
|
||||
Text string
|
||||
// Score — балл, выставленный организатором; nil — ещё не оценено.
|
||||
Score *int
|
||||
Score *int
|
||||
}
|
||||
|
||||
@@ -8,10 +8,8 @@ import (
|
||||
"github.com/jackc/pgx/v5/pgxpool"
|
||||
)
|
||||
|
||||
// ErrItemNotFound означает, что объект карты не найден.
|
||||
var ErrItemNotFound = errors.New("объект карты не найден")
|
||||
|
||||
// Item — объект на карте игры.
|
||||
type Item struct {
|
||||
GameID int
|
||||
Kind string
|
||||
@@ -21,7 +19,6 @@ type Item struct {
|
||||
Hidden bool
|
||||
}
|
||||
|
||||
// Repo работает с объектами карт в PostgreSQL.
|
||||
type Repo struct {
|
||||
pool *pgxpool.Pool
|
||||
}
|
||||
@@ -31,7 +28,6 @@ func NewGameMapRepo(pool *pgxpool.Pool) *Repo {
|
||||
return &Repo{pool: pool}
|
||||
}
|
||||
|
||||
// Ensure добавляет объект карты, если он ещё не существует.
|
||||
func (r *Repo) Ensure(ctx context.Context, gameID int, kind string, itemID int) error {
|
||||
_, err := r.pool.Exec(ctx, `
|
||||
INSERT INTO game_map_items (game_id, kind, item_id)
|
||||
@@ -44,7 +40,6 @@ func (r *Repo) Ensure(ctx context.Context, gameID int, kind string, itemID int)
|
||||
return err
|
||||
}
|
||||
|
||||
// List возвращает объекты карты игры.
|
||||
func (r *Repo) List(ctx context.Context, gameID int) ([]Item, error) {
|
||||
rows, err := r.pool.Query(
|
||||
ctx,
|
||||
@@ -75,7 +70,6 @@ func (r *Repo) List(ctx context.Context, gameID int) ([]Item, error) {
|
||||
return items, rows.Err()
|
||||
}
|
||||
|
||||
// Update изменяет координаты и видимость объекта карты.
|
||||
func (r *Repo) Update(ctx context.Context, item Item) error {
|
||||
result, err := r.pool.Exec(
|
||||
ctx,
|
||||
@@ -103,7 +97,6 @@ func (r *Repo) Update(ctx context.Context, item Item) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Delete удаляет объект карты.
|
||||
func (r *Repo) Delete(ctx context.Context, item Item) error {
|
||||
_, err := r.pool.Exec(
|
||||
ctx,
|
||||
|
||||
@@ -224,7 +224,6 @@ func (r *ScenariosRepo) GetScenarioByID(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Автор может отсутствовать (обезличен после удаления аккаунта).
|
||||
if authorID != nil && authorUsername != nil {
|
||||
scenario.Author = &repos.User{ID: *authorID, Username: *authorUsername}
|
||||
}
|
||||
@@ -232,7 +231,6 @@ func (r *ScenariosRepo) GetScenarioByID(
|
||||
return scenario, nil
|
||||
}
|
||||
|
||||
// UpdateScenarioByID меняет основные поля сценария.
|
||||
func (r *ScenariosRepo) UpdateScenarioByID(
|
||||
ctx context.Context,
|
||||
id int,
|
||||
@@ -263,12 +261,6 @@ func (r *ScenariosRepo) UpdateScenarioByID(
|
||||
return nil
|
||||
}
|
||||
|
||||
// UpdateScenarioStatusByID меняет статус сценария. allowPublished=true
|
||||
// разрешает менять статус опубликованного сценария (только для админа).
|
||||
// Возвращает false, если строка не обновлена (например, не-админ пытается
|
||||
// снять с публикации опубликованный сценарий). published_at фиксируется
|
||||
// только при ПЕРВОЙ публикации: повторная публикация не двигает «момент
|
||||
// отчуждения» исключительного права (п. 7.2 Пользовательского соглашения).
|
||||
func (r *ScenariosRepo) UpdateScenarioStatusByID(
|
||||
ctx context.Context,
|
||||
id int,
|
||||
@@ -296,10 +288,6 @@ func (r *ScenariosRepo) UpdateScenarioStatusByID(
|
||||
return tag.RowsAffected() > 0, nil
|
||||
}
|
||||
|
||||
// DeleteScenarioByID помечает сценарий удалённым (soft delete).
|
||||
// allowPublished=true разрешает удаление опубликованного сценария
|
||||
// (только для админа). Возвращает false, если строка не обновлена
|
||||
// (например, не-админ пытается удалить опубликованный сценарий).
|
||||
func (r *ScenariosRepo) DeleteScenarioByID(
|
||||
ctx context.Context,
|
||||
id int,
|
||||
@@ -347,7 +335,6 @@ func (r *ScenariosRepo) GetStoryByScenarioID(
|
||||
return story, nil
|
||||
}
|
||||
|
||||
// UpdateStoryByScenarioID обновляет содержимое (историю) сценария.
|
||||
func (r *ScenariosRepo) UpdateStoryByScenarioID(
|
||||
ctx context.Context,
|
||||
id int,
|
||||
|
||||
@@ -20,8 +20,6 @@ func NewTeamAnswersRepo(pool *pgxpool.Pool) *TeamAnswersRepo {
|
||||
}
|
||||
}
|
||||
|
||||
// UpsertAnswers сохраняет ответы команды: существующие перезаписываются,
|
||||
// отсутствующие — вставляются (UNIQUE (team_id, question_code)).
|
||||
func (r *TeamAnswersRepo) UpsertAnswers(
|
||||
ctx context.Context,
|
||||
teamID int,
|
||||
@@ -90,7 +88,6 @@ func (r *TeamAnswersRepo) GetAnswersByTeamID(
|
||||
return answers, nil
|
||||
}
|
||||
|
||||
// GetAnswersByGameID возвращает ответы всех команд игры (вместе с баллами).
|
||||
func (r *TeamAnswersRepo) GetAnswersByGameID(
|
||||
ctx context.Context,
|
||||
gameID int,
|
||||
@@ -134,10 +131,6 @@ func (r *TeamAnswersRepo) GetAnswersByGameID(
|
||||
return answers, nil
|
||||
}
|
||||
|
||||
// SetAnswerScore выставляет балл за ответ; score == nil сбрасывает балл.
|
||||
// Балл можно выставить и за неотвеченный вопрос (команда может не дать
|
||||
// ответ): если строки (team_id, question_code) ещё нет, она создаётся с
|
||||
// пустым ответом и выставленным баллом.
|
||||
func (r *TeamAnswersRepo) SetAnswerScore(
|
||||
ctx context.Context,
|
||||
teamID int,
|
||||
|
||||
@@ -86,7 +86,6 @@ func (r *TeamsRepo) AnswerParticipation(ctx context.Context, token, answer strin
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Условие проверяется повторно после снятия блокировки строки конкурентным ответом.
|
||||
_, err = r.pool.Exec(ctx, `WITH eligible_game AS (SELECT g.id FROM games g JOIN teams t ON t.game_id=g.id
|
||||
WHERE t.participation_token_hash=$1 AND g.status='draft' AND g.start_at > (clock_timestamp() AT TIME ZONE 'UTC') FOR SHARE OF g)
|
||||
UPDATE teams t SET participation_status=$2,
|
||||
|
||||
@@ -191,7 +191,6 @@ func (r *TeamsRepo) DeleteTeam(ctx context.Context, teamID int) error {
|
||||
// ErrRegistrationClosed indicates that team registration is unavailable.
|
||||
var ErrRegistrationClosed = errors.New("Игра не найдена или регистрация уже закрыта")
|
||||
|
||||
// RegisterTeam сохраняет одну команду на доставку, включая одновременные повторы.
|
||||
func (r *TeamsRepo) RegisterTeam(ctx context.Context, gameID int, deliveryID, name, email string, participantsCount int32, password string) (int, error) {
|
||||
return r.RegisterTeamWithParticipation(ctx, gameID, deliveryID, name, email, participantsCount, password, false)
|
||||
}
|
||||
@@ -199,7 +198,6 @@ func (r *TeamsRepo) RegisterTeam(ctx context.Context, gameID int, deliveryID, na
|
||||
// RegisterTeamWithParticipation registers a team and its participation data.
|
||||
func (r *TeamsRepo) RegisterTeamWithParticipation(ctx context.Context, gameID int, deliveryID, name, email string, participantsCount int32, password string, autoConfirm bool) (int, error) {
|
||||
var id int
|
||||
// Повтор уже принятой доставки успешен и после старта игры.
|
||||
err := r.pool.QueryRow(ctx, `SELECT id FROM teams WHERE game_id = $1 AND registration_delivery_id = $2`, gameID, deliveryID).Scan(&id)
|
||||
if err == nil {
|
||||
return id, nil
|
||||
@@ -223,7 +221,6 @@ func (r *TeamsRepo) RegisterTeamWithParticipation(ctx context.Context, gameID in
|
||||
return id, err
|
||||
}
|
||||
|
||||
// GetActiveTeamsByGameID исключает отменённые регистрации из игровых выгрузок и результатов.
|
||||
func (r *TeamsRepo) GetActiveTeamsByGameID(ctx context.Context, gameID int) ([]*repos.Team, error) {
|
||||
teams, err := r.GetTeamsByGameID(ctx, gameID)
|
||||
active := teams[:0]
|
||||
|
||||
@@ -1,16 +1,11 @@
|
||||
// Package user_agreements_repo persists user agreements.
|
||||
package user_agreements_repo
|
||||
|
||||
// AgreementType — типы документов, акцептуемых пользователем.
|
||||
const (
|
||||
AgreementTypeTerms = "terms" // Пользовательское соглашение
|
||||
AgreementTypePrivacy = "privacy" // Политика конфиденциальности
|
||||
AgreementTypeTerms = "terms"
|
||||
AgreementTypePrivacy = "privacy"
|
||||
)
|
||||
|
||||
// Agreement — фиксируемый факт акцепта документа (ст. 9 152-ФЗ — согласие в
|
||||
// форме, позволяющей подтвердить факт его получения). Version должна
|
||||
// соответствовать опубликованной редакции документа; при изменении текста
|
||||
// документа версия обязана увеличиваться.
|
||||
type Agreement struct {
|
||||
AgreementType string
|
||||
Version string
|
||||
|
||||
@@ -30,10 +30,6 @@ func NewUserRepo(pool *pgxpool.Pool) *UsersRepo {
|
||||
}
|
||||
}
|
||||
|
||||
// AddUserWithAgreements создаёт пользователя и фиксирует акцепты документов
|
||||
// (user_agreements) в одной транзакции: пользователь либо существует вместе с
|
||||
// записями о согласиях, либо не существует вовсе (доказуемое согласие,
|
||||
// ст. 9 152-ФЗ). Возвращает id созданного пользователя.
|
||||
func (s *UsersRepo) AddUserWithAgreements(
|
||||
ctx context.Context,
|
||||
username string,
|
||||
@@ -42,8 +38,6 @@ func (s *UsersRepo) AddUserWithAgreements(
|
||||
roles []string,
|
||||
agreements []user_agreements_repo.Agreement,
|
||||
) (int, error) {
|
||||
// Инвариант «пользователь не существует без записей о согласиях»
|
||||
// держится на том, что акцепты передаются всегда.
|
||||
if len(agreements) == 0 {
|
||||
return 0, errors.New("Не переданы записи об акцепте соглашений")
|
||||
}
|
||||
@@ -73,15 +67,11 @@ func (s *UsersRepo) AddUserWithAgreements(
|
||||
email,
|
||||
).Scan(&id)
|
||||
if err != nil {
|
||||
// Ничего не вставлено — занят username или email.
|
||||
if errors.Is(err, pgx.ErrNoRows) {
|
||||
return 0, errors.New("Username или email уже используется")
|
||||
}
|
||||
// Гонка: между проверкой NOT EXISTS и INSERT другой запрос успел
|
||||
// создать пользователя с тем же username/email — нарушение
|
||||
// уникальности. Возвращаем тот же понятный клиенту ответ.
|
||||
var pgErr *pgconn.PgError
|
||||
if errors.As(err, &pgErr) && pgErr.Code == "23505" { // нарушение уникальности
|
||||
if errors.As(err, &pgErr) && pgErr.Code == "23505" {
|
||||
return 0, errors.New("Username или email уже используется")
|
||||
}
|
||||
return 0, err
|
||||
@@ -266,12 +256,6 @@ func (s *UsersRepo) DeleteUserRole(
|
||||
return err
|
||||
}
|
||||
|
||||
// DeleteUser удаляет учётную запись и персональные данные пользователя
|
||||
// (ст. 21 152-ФЗ — уничтожение ПДн при удалении аккаунта). Выполняется в одной
|
||||
// транзакции. Публичный контент пользователя (сценарии, команды) сохраняется:
|
||||
// FK-ограничения с ON DELETE SET NULL (миграция
|
||||
// 20260818232725_keep_content_on_user_delete.sql) обнуляют привязку к автору —
|
||||
// контент остаётся в обезличенном виде.
|
||||
func (s *UsersRepo) DeleteUser(
|
||||
ctx context.Context,
|
||||
userID int,
|
||||
@@ -282,7 +266,6 @@ func (s *UsersRepo) DeleteUser(
|
||||
}
|
||||
defer tx.Rollback(ctx) //nolint:errcheck
|
||||
|
||||
// Refresh-токены (FK CASCADE, удаляем явно для предсказуемости).
|
||||
if _, err = tx.Exec(
|
||||
ctx,
|
||||
`DELETE FROM refresh_tokens WHERE user_id = $1`,
|
||||
@@ -291,13 +274,6 @@ func (s *UsersRepo) DeleteUser(
|
||||
return err
|
||||
}
|
||||
|
||||
// Зафиксированные акцепты соглашений НЕ удаляются: ст. 9 ч. 4 152-ФЗ
|
||||
// возлагает на оператора бремя доказывания получения согласия, поэтому
|
||||
// факт акцепта (документ, версия, время) сохраняется в обезличенном виде:
|
||||
// ссылка на пользователя и персональные данные внутри записи (ip,
|
||||
// user_agent) обнуляются. FK ON DELETE SET NULL (миграция
|
||||
// 20260818231943_create_user_agreements_table.sql) гарантирует, что при
|
||||
// удалении users строки акцептов остаются с user_id = NULL.
|
||||
if _, err = tx.Exec(
|
||||
ctx,
|
||||
`UPDATE user_agreements
|
||||
@@ -308,8 +284,6 @@ func (s *UsersRepo) DeleteUser(
|
||||
return err
|
||||
}
|
||||
|
||||
// Пользователь. Связанные сценарии (author_id) и команды (creator_id)
|
||||
// обнуляются автоматически через ON DELETE SET NULL.
|
||||
tag, err := tx.Exec(
|
||||
ctx,
|
||||
`DELETE FROM users WHERE id = $1`,
|
||||
|
||||
Reference in New Issue
Block a user