package storage import ( "context" "encoding/json" "time" ) // AdminPost — новость или статья для админки, включая скрытые. type AdminPost struct { ID int64 `json:"id"` Kind string `json:"kind"` Slug string `json:"slug"` Title string `json:"title"` Lead string `json:"lead"` Blocks json.RawMessage `json:"blocks"` ImageID *int64 `json:"image_id"` Image string `json:"image,omitempty"` Published bool `json:"published"` PublishedAt time.Time `json:"published_at"` SEOTitle string `json:"seo_title"` SEODesc string `json:"seo_description"` SEOH1 string `json:"seo_h1"` } // AdminPosts — публикации одного вида (news | article), новые сверху. func (s *Storage) AdminPosts(ctx context.Context, kind string) ([]AdminPost, error) { rows, err := s.pool.Query(ctx, ` SELECT p.id, p.kind, p.slug, p.title, p.lead, p.blocks, p.image_id, COALESCE(m.path, ''), p.published, p.published_at, p.seo_title, p.seo_description, p.seo_h1 FROM posts p LEFT JOIN media m ON m.id = p.image_id WHERE p.kind = $1 AND p.deleted_at IS NULL ORDER BY p.published_at DESC, p.id DESC`, kind) if err != nil { return nil, err } defer rows.Close() var out []AdminPost for rows.Next() { var a AdminPost if err := rows.Scan(&a.ID, &a.Kind, &a.Slug, &a.Title, &a.Lead, &a.Blocks, &a.ImageID, &a.Image, &a.Published, &a.PublishedAt, &a.SEOTitle, &a.SEODesc, &a.SEOH1); err != nil { return nil, err } out = append(out, a) } return out, rows.Err() } type PostInput struct { Kind string Slug string Title string Lead string Blocks json.RawMessage ImageID *int64 Published bool PublishedAt time.Time SEOTitle string SEODesc string SEOH1 string } func (s *Storage) AdminCreatePost(ctx context.Context, in PostInput) (int64, error) { var id int64 err := s.pool.QueryRow(ctx, ` INSERT INTO posts (kind, slug, title, lead, blocks, image_id, published, published_at, seo_title, seo_description, seo_h1) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11) RETURNING id`, in.Kind, in.Slug, in.Title, in.Lead, in.Blocks, in.ImageID, in.Published, in.PublishedAt, in.SEOTitle, in.SEODesc, in.SEOH1).Scan(&id) return id, wrapConflict(err) } // AdminUpdatePost меняет публикацию; вид (новость или статья) после создания не меняется. func (s *Storage) AdminUpdatePost(ctx context.Context, id int64, in PostInput) error { tag, err := s.pool.Exec(ctx, ` UPDATE posts SET slug=$3, title=$4, lead=$5, blocks=$6, image_id=$7, published=$8, published_at=$9, seo_title=$10, seo_description=$11, seo_h1=$12, updated_at=now() WHERE id=$1 AND kind=$2 AND deleted_at IS NULL`, id, in.Kind, in.Slug, in.Title, in.Lead, in.Blocks, in.ImageID, in.Published, in.PublishedAt, in.SEOTitle, in.SEODesc, in.SEOH1) if err != nil { return wrapConflict(err) } if tag.RowsAffected() == 0 { return ErrNotFound } return nil } // AdminDeletePost — мягкое удаление, как у акций. func (s *Storage) AdminDeletePost(ctx context.Context, kind string, id int64) error { tag, err := s.pool.Exec(ctx, ` UPDATE posts SET deleted_at=now(), published=false, updated_at=now() WHERE id=$1 AND kind=$2 AND deleted_at IS NULL`, id, kind) if err != nil { return err } if tag.RowsAffected() == 0 { return ErrNotFound } return nil }