From 4f056e91d40effb3296f1dd003188d135a577366 Mon Sep 17 00:00:00 2001 From: DiheChen Date: Sun, 8 Nov 2026 21:48:23 +0800 Subject: [PATCH] feat: store whether registration is open --- db/migrations/00005_site_registration.sql | 5 +++ db/queries/site.sql | 8 +++- internal/adapter/in/api/site.go | 4 +- .../out/postgres/internal/query/site.sql.go | 39 +++++++++++++------ internal/adapter/out/postgres/site.go | 19 ++++++--- internal/adapter/out/postgres/site_test.go | 32 ++++++++++----- internal/app/port/porttest/store.go | 18 ++++++--- internal/app/port/site.go | 10 ++++- internal/app/site/site.go | 20 +++++++--- internal/app/site/site_test.go | 27 +++++++------ 10 files changed, 127 insertions(+), 55 deletions(-) create mode 100644 db/migrations/00005_site_registration.sql diff --git a/db/migrations/00005_site_registration.sql b/db/migrations/00005_site_registration.sql new file mode 100644 index 0000000..3524845 --- /dev/null +++ b/db/migrations/00005_site_registration.sql @@ -0,0 +1,5 @@ +-- +goose Up +-- site 表没有行或者 brand_name 为 NULL 时,品牌名是默认的 Alisten;没有行时开放注册。 +ALTER TABLE site + ALTER COLUMN brand_name DROP NOT NULL, + ADD COLUMN registration_open boolean NOT NULL DEFAULT true; diff --git a/db/queries/site.sql b/db/queries/site.sql index 39dae89..c6f5dd1 100644 --- a/db/queries/site.sql +++ b/db/queries/site.sql @@ -1,6 +1,10 @@ --- name: BrandName :one -SELECT brand_name FROM site; +-- name: SiteSettings :one +SELECT brand_name, registration_open FROM site; -- name: SetBrandName :exec INSERT INTO site (brand_name) VALUES (@brand_name) ON CONFLICT (id) DO UPDATE SET brand_name = excluded.brand_name; + +-- name: SetRegistrationOpen :exec +INSERT INTO site (registration_open) VALUES (@registration_open) +ON CONFLICT (id) DO UPDATE SET registration_open = excluded.registration_open; diff --git a/internal/adapter/in/api/site.go b/internal/adapter/in/api/site.go index c4105c3..e3c4285 100644 --- a/internal/adapter/in/api/site.go +++ b/internal/adapter/in/api/site.go @@ -7,11 +7,11 @@ import ( ) func (s *server) getSite(ctx context.Context, _ call) (any, error) { - name, err := s.Site.BrandName(ctx) + settings, err := s.Site.Settings(ctx) if err != nil { return nil, err } - return protocol.GetSiteResponse{BrandName: name}, nil + return protocol.GetSiteResponse{BrandName: settings.BrandName}, nil } func (s *server) adminUpdateSite(ctx context.Context, c call) (any, error) { diff --git a/internal/adapter/out/postgres/internal/query/site.sql.go b/internal/adapter/out/postgres/internal/query/site.sql.go index b299ea7..8472f66 100644 --- a/internal/adapter/out/postgres/internal/query/site.sql.go +++ b/internal/adapter/out/postgres/internal/query/site.sql.go @@ -9,23 +9,38 @@ import ( "context" ) -const brandName = `-- name: BrandName :one -SELECT brand_name FROM site -` - -func (q *Queries) BrandName(ctx context.Context) (string, error) { - row := q.db.QueryRow(ctx, brandName) - var brand_name string - err := row.Scan(&brand_name) - return brand_name, err -} - const setBrandName = `-- name: SetBrandName :exec INSERT INTO site (brand_name) VALUES ($1) ON CONFLICT (id) DO UPDATE SET brand_name = excluded.brand_name ` -func (q *Queries) SetBrandName(ctx context.Context, brandName string) error { +func (q *Queries) SetBrandName(ctx context.Context, brandName *string) error { _, err := q.db.Exec(ctx, setBrandName, brandName) return err } + +const setRegistrationOpen = `-- name: SetRegistrationOpen :exec +INSERT INTO site (registration_open) VALUES ($1) +ON CONFLICT (id) DO UPDATE SET registration_open = excluded.registration_open +` + +func (q *Queries) SetRegistrationOpen(ctx context.Context, registrationOpen bool) error { + _, err := q.db.Exec(ctx, setRegistrationOpen, registrationOpen) + return err +} + +const siteSettings = `-- name: SiteSettings :one +SELECT brand_name, registration_open FROM site +` + +type SiteSettingsRow struct { + BrandName *string + RegistrationOpen bool +} + +func (q *Queries) SiteSettings(ctx context.Context) (SiteSettingsRow, error) { + row := q.db.QueryRow(ctx, siteSettings) + var i SiteSettingsRow + err := row.Scan(&i.BrandName, &i.RegistrationOpen) + return i, err +} diff --git a/internal/adapter/out/postgres/site.go b/internal/adapter/out/postgres/site.go index aab9d1f..dc205f0 100644 --- a/internal/adapter/out/postgres/site.go +++ b/internal/adapter/out/postgres/site.go @@ -12,20 +12,27 @@ import ( var _ port.Site = (*Store)(nil) -func (s *Store) BrandName(ctx context.Context) (string, error) { - name, err := s.q.BrandName(ctx) +func (s *Store) Settings(ctx context.Context) (port.SiteSettings, error) { + row, err := s.q.SiteSettings(ctx) if errors.Is(err, pgx.ErrNoRows) { - return "", nil + return port.SiteSettings{RegistrationOpen: true}, nil } if err != nil { - return "", fmt.Errorf("读取品牌名:%w", err) + return port.SiteSettings{}, fmt.Errorf("读取站点设置:%w", err) } - return name, nil + return port.SiteSettings{BrandName: orEmpty(row.BrandName), RegistrationOpen: row.RegistrationOpen}, nil } func (s *Store) SetBrandName(ctx context.Context, name string) error { - if err := s.q.SetBrandName(ctx, name); err != nil { + if err := s.q.SetBrandName(ctx, &name); err != nil { return fmt.Errorf("保存品牌名:%w", err) } return nil } + +func (s *Store) SetRegistrationOpen(ctx context.Context, open bool) error { + if err := s.q.SetRegistrationOpen(ctx, open); err != nil { + return fmt.Errorf("保存注册开关:%w", err) + } + return nil +} diff --git a/internal/adapter/out/postgres/site_test.go b/internal/adapter/out/postgres/site_test.go index 8cc9391..7a910e3 100644 --- a/internal/adapter/out/postgres/site_test.go +++ b/internal/adapter/out/postgres/site_test.go @@ -1,20 +1,34 @@ package postgres -import "testing" +import ( + "testing" -func TestBrandName(t *testing.T) { + "github.com/dihemoe/alisten/internal/app/port" +) + +func TestSiteSettings(t *testing.T) { store, pool := newStore(t) - if name, err := store.BrandName(t.Context()); err != nil || name != "" { - t.Fatalf("没有设置过时 = %q, %v,期望空字符串", name, err) + check := func(want port.SiteSettings, why string) { + t.Helper() + if got, err := store.Settings(t.Context()); err != nil || got != want { + t.Errorf("%s:Settings = %#v, %v,期望 %#v", why, got, err, want) + } } - for _, want := range []string{"周末听歌会", "Alisten"} { - if err := store.SetBrandName(t.Context(), want); err != nil { + check(port.SiteSettings{RegistrationOpen: true}, "没有设置过时没有品牌名、开放注册") + if err := store.SetRegistrationOpen(t.Context(), false); err != nil { + t.Fatal(err) + } + check(port.SiteSettings{RegistrationOpen: false}, "只关闭注册时品牌名仍未设置") + for _, name := range []string{"周末听歌会", "Alisten"} { + if err := store.SetBrandName(t.Context(), name); err != nil { t.Fatal(err) } - if got, err := store.BrandName(t.Context()); err != nil || got != want { - t.Errorf("BrandName = %q, %v,期望 %q", got, err, want) - } + check(port.SiteSettings{BrandName: name, RegistrationOpen: false}, "修改品牌名不影响注册开关") } + if err := store.SetRegistrationOpen(t.Context(), true); err != nil { + t.Fatal(err) + } + check(port.SiteSettings{BrandName: "Alisten", RegistrationOpen: true}, "重新开放注册不影响品牌名") var rows int if err := pool.QueryRow(t.Context(), "SELECT count(*) FROM site").Scan(&rows); err != nil { t.Fatal(err) diff --git a/internal/app/port/porttest/store.go b/internal/app/port/porttest/store.go index 630df77..1ae7b8a 100644 --- a/internal/app/port/porttest/store.go +++ b/internal/app/port/porttest/store.go @@ -29,9 +29,10 @@ type Store struct { states map[string]*roomState tickets map[string]storedTicket // 房间 ID 到 bot 成员 ID。 - bots map[string]string - cookies map[string]string - brandName string + bots map[string]string + cookies map[string]string + brandName string + registrationClosed bool } type storedMember struct { @@ -425,10 +426,17 @@ func (s *Store) SetBilibiliCredential(_ context.Context, credential string) erro return nil } -func (s *Store) BrandName(context.Context) (string, error) { +func (s *Store) Settings(context.Context) (port.SiteSettings, error) { s.mu.Lock() defer s.mu.Unlock() - return s.brandName, nil + return port.SiteSettings{BrandName: s.brandName, RegistrationOpen: !s.registrationClosed}, nil +} + +func (s *Store) SetRegistrationOpen(_ context.Context, open bool) error { + s.mu.Lock() + defer s.mu.Unlock() + s.registrationClosed = !open + return nil } func (s *Store) SetBrandName(_ context.Context, name string) error { diff --git a/internal/app/port/site.go b/internal/app/port/site.go index 6c015ec..194832b 100644 --- a/internal/app/port/site.go +++ b/internal/app/port/site.go @@ -2,8 +2,14 @@ package port import "context" -// 没有设置过品牌名时,BrandName 返回空字符串。 +type SiteSettings struct { + // 没有设置过品牌名时为空字符串。 + BrandName string + RegistrationOpen bool +} + type Site interface { - BrandName(ctx context.Context) (string, error) + Settings(ctx context.Context) (SiteSettings, error) SetBrandName(ctx context.Context, name string) error + SetRegistrationOpen(ctx context.Context, open bool) error } diff --git a/internal/app/site/site.go b/internal/app/site/site.go index 095d53b..cba8ecc 100644 --- a/internal/app/site/site.go +++ b/internal/app/site/site.go @@ -25,15 +25,25 @@ func New(site port.Site) *Service { return &Service{site: site} } -func (s *Service) BrandName(ctx context.Context) (string, error) { - name, err := s.site.BrandName(ctx) +type Settings struct { + BrandName string + RegistrationOpen bool +} + +func (s *Service) Settings(ctx context.Context) (Settings, error) { + settings, err := s.site.Settings(ctx) if err != nil { - return "", err + return Settings{}, err } + name := settings.BrandName if name == "" { - return defaultBrandName, nil + name = defaultBrandName } - return name, nil + return Settings{BrandName: name, RegistrationOpen: settings.RegistrationOpen}, nil +} + +func (s *Service) SetRegistrationOpen(ctx context.Context, open bool) error { + return s.site.SetRegistrationOpen(ctx, open) } func (s *Service) SetBrandName(ctx context.Context, name string) (string, error) { diff --git a/internal/app/site/site_test.go b/internal/app/site/site_test.go index b84c39a..4e6fd01 100644 --- a/internal/app/site/site_test.go +++ b/internal/app/site/site_test.go @@ -10,16 +10,19 @@ import ( "github.com/dihemoe/alisten/internal/app/port/porttest" ) -func TestBrandName(t *testing.T) { +func TestSettings(t *testing.T) { s := New(porttest.New()) - if got, err := s.BrandName(t.Context()); err != nil || got != "Alisten" { - t.Fatalf("没有设置过时 = %q, %v,期望默认的 Alisten", got, err) + if got, err := s.Settings(t.Context()); err != nil || got != (Settings{BrandName: "Alisten", RegistrationOpen: true}) { + t.Fatalf("没有设置过时 = %#v, %v,期望默认的 Alisten、开放注册", got, err) } if saved, err := s.SetBrandName(t.Context(), " 周末听歌会\n"); err != nil || saved != "周末听歌会" { t.Fatalf("SetBrandName = %q, %v,期望返回去掉首尾空白的 %q", saved, err, "周末听歌会") } - if got, err := s.BrandName(t.Context()); err != nil || got != "周末听歌会" { - t.Errorf("设置之后 = %q, %v,期望去掉首尾空白的 %q", got, err, "周末听歌会") + if err := s.SetRegistrationOpen(t.Context(), false); err != nil { + t.Fatal(err) + } + if got, err := s.Settings(t.Context()); err != nil || got != (Settings{BrandName: "周末听歌会", RegistrationOpen: false}) { + t.Errorf("设置之后 = %#v, %v", got, err) } } @@ -42,8 +45,8 @@ func TestInvalidBrandName(t *testing.T) { t.Errorf("SetBrandName(%q) = %v,期望 ErrInvalidBrandName", name, err) } } - if got, err := s.BrandName(t.Context()); err != nil || got != "周末听歌会" { - t.Errorf("被拒绝之后 = %q, %v,期望仍是 %q", got, err, "周末听歌会") + if got, err := s.Settings(t.Context()); err != nil || got.BrandName != "周末听歌会" { + t.Errorf("被拒绝之后 = %q, %v,期望仍是 %q", got.BrandName, err, "周末听歌会") } } @@ -63,12 +66,12 @@ type brokenSite struct{ port.Site } var errBroken = errors.New("连接中断") -func (brokenSite) BrandName(context.Context) (string, error) { - return "", errBroken +func (brokenSite) Settings(context.Context) (port.SiteSettings, error) { + return port.SiteSettings{}, errBroken } -func TestBrandNameReadFailure(t *testing.T) { - if got, err := New(brokenSite{}).BrandName(t.Context()); !errors.Is(err, errBroken) { - t.Errorf("读取失败时 = %q, %v,期望返回错误而不是默认的品牌名", got, err) +func TestSettingsReadFailure(t *testing.T) { + if got, err := New(brokenSite{}).Settings(t.Context()); !errors.Is(err, errBroken) { + t.Errorf("读取失败时 = %#v, %v,期望返回错误而不是默认的品牌名", got, err) } }