Files
geo/server/internal/ops/repository/site_domain_mapping.go
T
root 7d8e82c69f feat(ops): add object storage management and site-mapping CSV import/export
Ship the Ops backstage 对象存储 page (browse / upload / move / delete /
folder ops) backed by a new object-storage service + handler, with the
shared storage client extended with List/Stat/Copy/Usage/DownloadURL so
both MinIO and Aliyun providers cover the new surface. Add
ForcePathStyle to the object-storage config and treat r2/s3/custom_s3
as external providers so Cloudflare R2 and other S3-compatibles can
share the MinIO client path without spinning up the bundled MinIO.

Also add CSV import/export + template download to the site-domain
mappings page, raise gin MaxMultipartMemory to 8 MiB for the new
multipart endpoints, register Ops swagger routes, and extend the
descriptions-parity test to cover the ops router.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 01:19:01 +08:00

251 lines
6.7 KiB
Go

package repository
import (
"context"
"errors"
"fmt"
"github.com/jackc/pgx/v5"
"github.com/jackc/pgx/v5/pgxpool"
"github.com/geo-platform/tenant-api/internal/ops/domain"
)
var ErrSiteDomainMappingNotFound = errors.New("site domain mapping not found")
type SiteDomainMappingRepository struct {
pool *pgxpool.Pool
}
func NewSiteDomainMappingRepository(pool *pgxpool.Pool) *SiteDomainMappingRepository {
return &SiteDomainMappingRepository{pool: pool}
}
const siteDomainMappingColumns = `id, registrable_domain, site_key, site_name, is_active, created_at, updated_at`
type SiteDomainMappingFilter struct {
Keyword string
IsActive *bool
Limit int
Offset int
}
type CreateSiteDomainMappingInput struct {
RegistrableDomain string
SiteKey string
SiteName string
IsActive bool
}
type UpdateSiteDomainMappingInput struct {
RegistrableDomain string
SiteKey string
SiteName string
IsActive bool
}
type UpsertSiteDomainMappingInput struct {
RegistrableDomain string
SiteKey string
SiteName string
IsActive bool
}
type UpsertSiteDomainMappingResult struct {
Item *domain.SiteDomainMapping
Created bool
}
func scanSiteDomainMapping(row pgx.Row) (*domain.SiteDomainMapping, error) {
var item domain.SiteDomainMapping
err := row.Scan(
&item.ID,
&item.RegistrableDomain,
&item.SiteKey,
&item.SiteName,
&item.IsActive,
&item.CreatedAt,
&item.UpdatedAt,
)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrSiteDomainMappingNotFound
}
return nil, err
}
return &item, nil
}
func scanSiteDomainMappingUpsert(row pgx.Row) (*UpsertSiteDomainMappingResult, error) {
var item domain.SiteDomainMapping
var created bool
err := row.Scan(
&item.ID,
&item.RegistrableDomain,
&item.SiteKey,
&item.SiteName,
&item.IsActive,
&item.CreatedAt,
&item.UpdatedAt,
&created,
)
if err != nil {
if errors.Is(err, pgx.ErrNoRows) {
return nil, ErrSiteDomainMappingNotFound
}
return nil, err
}
return &UpsertSiteDomainMappingResult{Item: &item, Created: created}, nil
}
func buildSiteDomainMappingWhere(f SiteDomainMappingFilter) (string, []any) {
args := []any{}
where := "1=1"
if f.Keyword != "" {
args = append(args, "%"+f.Keyword+"%")
where += fmt.Sprintf(" AND (registrable_domain ILIKE $%d OR site_key ILIKE $%d OR site_name ILIKE $%d)",
len(args), len(args), len(args))
}
if f.IsActive != nil {
args = append(args, *f.IsActive)
where += fmt.Sprintf(" AND is_active = $%d", len(args))
}
return where, args
}
func (r *SiteDomainMappingRepository) List(ctx context.Context, f SiteDomainMappingFilter) ([]domain.SiteDomainMapping, int64, error) {
where, args := buildSiteDomainMappingWhere(f)
var total int64
if err := r.pool.QueryRow(ctx, "SELECT COUNT(*) FROM site_domain_mappings WHERE "+where, args...).Scan(&total); err != nil {
return nil, 0, err
}
limit := f.Limit
if limit <= 0 || limit > 200 {
limit = 20
}
offset := f.Offset
if offset < 0 {
offset = 0
}
args = append(args, limit, offset)
limitArg := len(args) - 1
offsetArg := len(args)
rows, err := r.pool.Query(ctx, fmt.Sprintf(`
SELECT %s
FROM site_domain_mappings
WHERE %s
ORDER BY is_active DESC, registrable_domain ASC, id DESC
LIMIT $%d OFFSET $%d`, siteDomainMappingColumns, where, limitArg, offsetArg), args...)
if err != nil {
return nil, 0, err
}
defer rows.Close()
items := make([]domain.SiteDomainMapping, 0, limit)
for rows.Next() {
item, scanErr := scanSiteDomainMapping(rows)
if scanErr != nil {
return nil, 0, scanErr
}
items = append(items, *item)
}
return items, total, rows.Err()
}
func (r *SiteDomainMappingRepository) Export(ctx context.Context, f SiteDomainMappingFilter) ([]domain.SiteDomainMapping, error) {
where, args := buildSiteDomainMappingWhere(f)
limit := f.Limit
if limit <= 0 {
limit = 10000
}
args = append(args, limit)
limitArg := len(args)
rows, err := r.pool.Query(ctx, fmt.Sprintf(`
SELECT %s
FROM site_domain_mappings
WHERE %s
ORDER BY is_active DESC, registrable_domain ASC, id DESC
LIMIT $%d`, siteDomainMappingColumns, where, limitArg), args...)
if err != nil {
return nil, err
}
defer rows.Close()
items := make([]domain.SiteDomainMapping, 0, limit)
for rows.Next() {
item, scanErr := scanSiteDomainMapping(rows)
if scanErr != nil {
return nil, scanErr
}
items = append(items, *item)
}
return items, rows.Err()
}
func (r *SiteDomainMappingRepository) GetByID(ctx context.Context, id int64) (*domain.SiteDomainMapping, error) {
return scanSiteDomainMapping(r.pool.QueryRow(ctx, `
SELECT `+siteDomainMappingColumns+`
FROM site_domain_mappings
WHERE id = $1`, id))
}
func (r *SiteDomainMappingRepository) Create(ctx context.Context, in CreateSiteDomainMappingInput) (*domain.SiteDomainMapping, error) {
return scanSiteDomainMapping(r.pool.QueryRow(ctx, `
INSERT INTO site_domain_mappings (registrable_domain, site_key, site_name, is_active)
VALUES ($1, $2, $3, $4)
RETURNING `+siteDomainMappingColumns,
in.RegistrableDomain, in.SiteKey, in.SiteName, in.IsActive))
}
func (r *SiteDomainMappingRepository) Update(ctx context.Context, id int64, in UpdateSiteDomainMappingInput) (*domain.SiteDomainMapping, error) {
return scanSiteDomainMapping(r.pool.QueryRow(ctx, `
UPDATE site_domain_mappings
SET registrable_domain = $1,
site_key = $2,
site_name = $3,
is_active = $4,
updated_at = NOW()
WHERE id = $5
RETURNING `+siteDomainMappingColumns,
in.RegistrableDomain, in.SiteKey, in.SiteName, in.IsActive, id))
}
func (r *SiteDomainMappingRepository) Upsert(ctx context.Context, in UpsertSiteDomainMappingInput) (*UpsertSiteDomainMappingResult, error) {
return scanSiteDomainMappingUpsert(r.pool.QueryRow(ctx, `
INSERT INTO site_domain_mappings (registrable_domain, site_key, site_name, is_active)
VALUES ($1, $2, $3, $4)
ON CONFLICT (registrable_domain) DO UPDATE
SET site_key = EXCLUDED.site_key,
site_name = EXCLUDED.site_name,
is_active = EXCLUDED.is_active,
updated_at = NOW()
RETURNING `+siteDomainMappingColumns+`, xmax = 0`,
in.RegistrableDomain, in.SiteKey, in.SiteName, in.IsActive))
}
func (r *SiteDomainMappingRepository) SetActive(ctx context.Context, id int64, active bool) (*domain.SiteDomainMapping, error) {
return scanSiteDomainMapping(r.pool.QueryRow(ctx, `
UPDATE site_domain_mappings
SET is_active = $1,
updated_at = NOW()
WHERE id = $2
RETURNING `+siteDomainMappingColumns, active, id))
}
func (r *SiteDomainMappingRepository) Delete(ctx context.Context, id int64) error {
cmd, err := r.pool.Exec(ctx, `DELETE FROM site_domain_mappings WHERE id = $1`, id)
if err != nil {
return err
}
if cmd.RowsAffected() == 0 {
return ErrSiteDomainMappingNotFound
}
return nil
}