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>
This commit is contained in:
@@ -44,6 +44,18 @@ type UpdateSiteDomainMappingInput struct {
|
||||
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(
|
||||
@@ -64,7 +76,29 @@ func scanSiteDomainMapping(row pgx.Row) (*domain.SiteDomainMapping, error) {
|
||||
return &item, nil
|
||||
}
|
||||
|
||||
func (r *SiteDomainMappingRepository) List(ctx context.Context, f SiteDomainMappingFilter) ([]domain.SiteDomainMapping, int64, error) {
|
||||
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"
|
||||
|
||||
@@ -78,6 +112,12 @@ func (r *SiteDomainMappingRepository) List(ctx context.Context, f SiteDomainMapp
|
||||
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
|
||||
@@ -117,6 +157,37 @@ func (r *SiteDomainMappingRepository) List(ctx context.Context, f SiteDomainMapp
|
||||
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+`
|
||||
@@ -145,6 +216,19 @@ func (r *SiteDomainMappingRepository) Update(ctx context.Context, id int64, in U
|
||||
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
|
||||
|
||||
Reference in New Issue
Block a user