owpengram-server/internal/store/postgres/themes.go

248 lines
6.7 KiB
Go

package postgres
import (
"context"
"encoding/json"
"errors"
"fmt"
"strings"
"time"
"github.com/jackc/pgx/v5"
"telesrv/internal/domain"
"telesrv/internal/store/postgres/sqlcgen"
)
// ThemeStore 用 PostgreSQL 实现 store.ThemeStore(裸 pgx,与近期功能一致,不走 sqlc)。
type ThemeStore struct {
db sqlcgen.DBTX
}
// NewThemeStore 基于 pgx 连接池(或事务)创建 ThemeStore。
func NewThemeStore(db sqlcgen.DBTX) *ThemeStore {
return &ThemeStore{db: db}
}
const themeColumns = `id, access_hash, creator_user_id, slug, title, emoticon, for_chat, document_id, settings, installs_count, created_at`
func (s *ThemeStore) CreateTheme(ctx context.Context, t domain.Theme) error {
if t.ID == 0 || t.CreatorUserID == 0 {
return domain.ErrThemeInvalid
}
settings, err := marshalThemeSettings(t.Settings)
if err != nil {
return err
}
createdAt := time.Now()
if t.CreatedAt > 0 {
createdAt = time.Unix(t.CreatedAt, 0)
}
_, err = s.db.Exec(ctx, `
INSERT INTO themes (
id, access_hash, creator_user_id, slug, title, emoticon, for_chat, document_id, settings, installs_count, created_at
) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)`,
t.ID, t.AccessHash, t.CreatorUserID, t.Slug, t.Title, t.Emoticon, t.ForChat,
t.DocumentID, settings, t.InstallsCount, createdAt,
)
if err != nil {
if isUniqueViolation(err) {
return domain.ErrThemeSlugTaken
}
return fmt.Errorf("insert theme: %w", err)
}
return nil
}
func scanTheme(row pgx.Row) (domain.Theme, error) {
var (
t domain.Theme
settings []byte
createdAt time.Time
)
if err := row.Scan(
&t.ID, &t.AccessHash, &t.CreatorUserID, &t.Slug, &t.Title, &t.Emoticon, &t.ForChat,
&t.DocumentID, &settings, &t.InstallsCount, &createdAt,
); err != nil {
return domain.Theme{}, err
}
t.CreatedAt = createdAt.Unix()
parsed, err := unmarshalThemeSettings(settings)
if err != nil {
return domain.Theme{}, err
}
t.Settings = parsed
return t, nil
}
func (s *ThemeStore) GetThemeByID(ctx context.Context, id int64) (domain.Theme, bool, error) {
row := s.db.QueryRow(ctx, `SELECT `+themeColumns+` FROM themes WHERE id = $1`, id)
t, err := scanTheme(row)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return domain.Theme{}, false, nil
}
return domain.Theme{}, false, fmt.Errorf("get theme by id: %w", err)
}
return t, true, nil
}
func (s *ThemeStore) GetThemeBySlug(ctx context.Context, slug string) (domain.Theme, bool, error) {
if slug == "" {
return domain.Theme{}, false, nil
}
row := s.db.QueryRow(ctx, `SELECT `+themeColumns+` FROM themes WHERE slug = $1`, slug)
t, err := scanTheme(row)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return domain.Theme{}, false, nil
}
return domain.Theme{}, false, fmt.Errorf("get theme by slug: %w", err)
}
return t, true, nil
}
func (s *ThemeStore) SlugExists(ctx context.Context, slug string) (bool, error) {
if slug == "" {
return false, nil
}
var exists bool
if err := s.db.QueryRow(ctx, `SELECT EXISTS(SELECT 1 FROM themes WHERE slug = $1)`, slug).Scan(&exists); err != nil {
return false, fmt.Errorf("theme slug exists: %w", err)
}
return exists, nil
}
func (s *ThemeStore) UpdateTheme(ctx context.Context, t domain.Theme) error {
settings, err := marshalThemeSettings(t.Settings)
if err != nil {
return err
}
tag, err := s.db.Exec(ctx, `
UPDATE themes SET slug = $2, title = $3, emoticon = $4, for_chat = $5, document_id = $6, settings = $7
WHERE id = $1`,
t.ID, t.Slug, t.Title, t.Emoticon, t.ForChat, t.DocumentID, settings,
)
if err != nil {
if isUniqueViolation(err) {
return domain.ErrThemeSlugTaken
}
return fmt.Errorf("update theme: %w", err)
}
if tag.RowsAffected() == 0 {
return domain.ErrThemeNotFound
}
return nil
}
func (s *ThemeStore) IncrementInstalls(ctx context.Context, id int64) error {
tag, err := s.db.Exec(ctx, `UPDATE themes SET installs_count = installs_count + 1 WHERE id = $1`, id)
if err != nil {
return fmt.Errorf("increment theme installs: %w", err)
}
if tag.RowsAffected() == 0 {
return domain.ErrThemeNotFound
}
return nil
}
func (s *ThemeStore) SetInstalled(ctx context.Context, userID, themeID int64, dark bool) error {
_, err := s.db.Exec(ctx, `
INSERT INTO theme_user_installs (user_id, theme_id, dark, installed_at)
VALUES ($1,$2,$3, now())
ON CONFLICT (user_id, theme_id) DO UPDATE SET dark = EXCLUDED.dark`,
userID, themeID, dark,
)
if err != nil {
if isUniqueViolation(err) {
return domain.ErrThemeNotFound // FK 缺失等同主题不存在
}
return fmt.Errorf("set theme installed: %w", err)
}
return nil
}
func (s *ThemeStore) RemoveInstalled(ctx context.Context, userID, themeID int64) error {
_, err := s.db.Exec(ctx, `DELETE FROM theme_user_installs WHERE user_id = $1 AND theme_id = $2`, userID, themeID)
if err != nil {
return fmt.Errorf("remove theme installed: %w", err)
}
return nil
}
func (s *ThemeStore) ListInstalledByUser(ctx context.Context, userID int64) ([]domain.Theme, error) {
rows, err := s.db.Query(ctx, `
SELECT `+prefixThemeColumns()+`
FROM themes t
JOIN theme_user_installs i ON i.theme_id = t.id
WHERE i.user_id = $1
ORDER BY i.installed_at, t.id`, userID)
if err != nil {
return nil, fmt.Errorf("list installed themes: %w", err)
}
defer rows.Close()
out := make([]domain.Theme, 0)
for rows.Next() {
t, err := scanTheme(rows)
if err != nil {
return nil, fmt.Errorf("scan installed theme: %w", err)
}
out = append(out, t)
}
return out, rows.Err()
}
func (s *ThemeStore) ListThemesForUser(ctx context.Context, userID int64) ([]domain.Theme, error) {
rows, err := s.db.Query(ctx, `
SELECT `+themeColumns+`
FROM themes
WHERE creator_user_id = $1
OR id IN (SELECT theme_id FROM theme_user_installs WHERE user_id = $1)
ORDER BY created_at, id`, userID)
if err != nil {
return nil, fmt.Errorf("list themes for user: %w", err)
}
defer rows.Close()
out := make([]domain.Theme, 0)
for rows.Next() {
t, err := scanTheme(rows)
if err != nil {
return nil, fmt.Errorf("scan user theme: %w", err)
}
out = append(out, t)
}
return out, rows.Err()
}
func prefixThemeColumns() string {
parts := strings.Split(themeColumns, ", ")
for i, p := range parts {
parts[i] = "t." + p
}
return strings.Join(parts, ", ")
}
func marshalThemeSettings(settings []domain.ThemeSettingsSpec) ([]byte, error) {
if len(settings) == 0 {
return []byte("[]"), nil
}
b, err := json.Marshal(settings)
if err != nil {
return nil, fmt.Errorf("marshal theme settings: %w", err)
}
return b, nil
}
func unmarshalThemeSettings(b []byte) ([]domain.ThemeSettingsSpec, error) {
if len(b) == 0 {
return nil, nil
}
var out []domain.ThemeSettingsSpec
if err := json.Unmarshal(b, &out); err != nil {
return nil, fmt.Errorf("unmarshal theme settings: %w", err)
}
if len(out) == 0 {
return nil, nil
}
return out, nil
}