- new internal/sysadmin package (overview/kb/features/settings) with small
port interfaces; business packages stay unaware of sysadmin
- new routes /api/v1/admin/system/{overview,kb,features,settings} guarded
by requireSystemAdmin
- goose 00049: system_settings table (key/jsonb value, updated_by/at)
- SettingSpec whitelist registry with DB > env > default precedence
- register sysadmin in module-boundaries.yaml (archtest rules)
57 lines
1.7 KiB
Go
57 lines
1.7 KiB
Go
package sysadmin
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"time"
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
"github.com/jackc/pgx/v5/pgxpool"
|
|
)
|
|
|
|
// ErrSettingNotFound DB 中无该 key 的行(回退 env/default)。
|
|
var ErrSettingNotFound = errors.New("sysadmin: setting not found")
|
|
|
|
// SettingsStore system_settings 表读写(表 owner 为 sysadmin)。
|
|
type SettingsStore interface {
|
|
Get(ctx context.Context, key string) (value json.RawMessage, updatedBy *int64, updatedAt *time.Time, err error)
|
|
Upsert(ctx context.Context, key string, value json.RawMessage, updatedBy int64) (time.Time, error)
|
|
}
|
|
|
|
type pgSettingsStore struct {
|
|
pool *pgxpool.Pool
|
|
}
|
|
|
|
func newPGSettingsStore(pool *pgxpool.Pool) SettingsStore {
|
|
return &pgSettingsStore{pool: pool}
|
|
}
|
|
|
|
func (s *pgSettingsStore) Get(ctx context.Context, key string) (json.RawMessage, *int64, *time.Time, error) {
|
|
var raw []byte
|
|
var updatedBy *int64
|
|
var updatedAt time.Time
|
|
err := s.pool.QueryRow(ctx, `
|
|
SELECT value, updated_by, updated_at FROM system_settings WHERE key = $1
|
|
`, key).Scan(&raw, &updatedBy, &updatedAt)
|
|
if err != nil {
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
return nil, nil, nil, ErrSettingNotFound
|
|
}
|
|
return nil, nil, nil, err
|
|
}
|
|
return json.RawMessage(raw), updatedBy, &updatedAt, nil
|
|
}
|
|
|
|
func (s *pgSettingsStore) Upsert(ctx context.Context, key string, value json.RawMessage, updatedBy int64) (time.Time, error) {
|
|
var updatedAt time.Time
|
|
err := s.pool.QueryRow(ctx, `
|
|
INSERT INTO system_settings (key, value, updated_by, updated_at)
|
|
VALUES ($1, $2, $3, now())
|
|
ON CONFLICT (key) DO UPDATE
|
|
SET value = EXCLUDED.value, updated_by = EXCLUDED.updated_by, updated_at = now()
|
|
RETURNING updated_at
|
|
`, key, []byte(value), updatedBy).Scan(&updatedAt)
|
|
return updatedAt, err
|
|
}
|