Files
ttstd-infinite-canvas/backend/internal/repository/admin_storage.go

197 lines
6.3 KiB
Go

package repository
import (
"strings"
"infinite-canvas/backend/internal/model"
)
type AdminResourceFilter struct {
Kind string
Status string
Provider string
UserQuery string
Keyword string
Limit int
Offset int
}
type ResourceKindStat struct {
Kind string `json:"kind" gorm:"column:kind"`
Count int64 `json:"count" gorm:"column:count"`
LogicalBytes int64 `json:"logicalBytes" gorm:"column:logical_bytes"`
PhysicalBytes int64 `json:"physicalBytes" gorm:"column:physical_bytes"`
}
type ResourceProviderStat struct {
Provider string `json:"provider" gorm:"column:provider"`
Count int64 `json:"count" gorm:"column:count"`
LogicalBytes int64 `json:"logicalBytes" gorm:"column:logical_bytes"`
PhysicalBytes int64 `json:"physicalBytes" gorm:"column:physical_bytes"`
}
type ResourceStorageSummary struct {
ResourceCount int64 `json:"resourceCount" gorm:"column:resource_count"`
ReadyCount int64 `json:"readyCount" gorm:"column:ready_count"`
LogicalBytes int64 `json:"logicalBytes" gorm:"column:logical_bytes"`
PhysicalBytes int64 `json:"physicalBytes" gorm:"column:physical_bytes"`
LocalBytes int64 `json:"localBytes" gorm:"column:local_bytes"`
RemoteBytes int64 `json:"remoteBytes" gorm:"column:remote_bytes"`
}
func (r *Repository) AdminResources(filter AdminResourceFilter) ([]model.Resource, int64, error) {
var resources []model.Resource
var total int64
query := r.db.Model(&model.Resource{})
if filter.Kind != "" {
query = query.Where("kind = ?", filter.Kind)
}
if filter.Status != "" {
query = query.Where("status = ?", filter.Status)
}
if filter.Provider != "" {
query = query.Where(resourceProviderExpression()+" = ?", filter.Provider)
}
if filter.UserQuery != "" {
pattern := "%" + strings.ToLower(filter.UserQuery) + "%"
matchingUsers := r.db.Model(&model.User{}).Select("id").Where(
"lower(id) LIKE ? OR lower(username) LIKE ? OR lower(display_name) LIKE ? OR lower(email) LIKE ?",
pattern, pattern, pattern, pattern,
)
// 保留对已删除用户资源的 ID 搜索,同时支持按现存用户的账号信息筛选。
query = query.Where("(lower(user_id) LIKE ? OR user_id IN (?))", pattern, matchingUsers)
}
if filter.Keyword != "" {
pattern := "%" + strings.ToLower(filter.Keyword) + "%"
query = query.Where("lower(id) LIKE ? OR lower(object_key) LIKE ?", pattern, pattern)
}
if err := query.Count(&total).Error; err != nil {
return nil, 0, err
}
if err := query.Order("created_at desc, id desc").Limit(filter.Limit).Offset(filter.Offset).Find(&resources).Error; err != nil {
return nil, 0, err
}
return resources, total, nil
}
func (r *Repository) UsersByIDs(ids []string) (map[string]model.User, error) {
if len(ids) == 0 {
return map[string]model.User{}, nil
}
var users []model.User
if err := r.db.Select("id", "username", "display_name", "email").Where("id IN ?", ids).Find(&users).Error; err != nil {
return nil, err
}
result := make(map[string]model.User, len(users))
for _, user := range users {
result[user.ID] = user
}
return result, nil
}
func (r *Repository) ResourceStorageSummary() (ResourceStorageSummary, error) {
var summary ResourceStorageSummary
err := r.db.Model(&model.Resource{}).Select(`
COUNT(*) AS resource_count,
COALESCE(SUM(CASE WHEN status = 'ready' THEN 1 ELSE 0 END), 0) AS ready_count,
COALESCE(SUM(size), 0) AS logical_bytes
`).Scan(&summary).Error
if err != nil {
return summary, err
}
physicalBytes, err := r.resourcePhysicalBytes("")
if err != nil {
return summary, err
}
localBytes, err := r.resourcePhysicalBytes(resourceProviderExpression()+" = ?", "local")
if err != nil {
return summary, err
}
summary.PhysicalBytes = physicalBytes
summary.LocalBytes = localBytes
summary.RemoteBytes = physicalBytes - localBytes
return summary, nil
}
func (r *Repository) resourcePhysicalBytes(where string, args ...any) (int64, error) {
query := r.db.Model(&model.Resource{}).
Select("MAX(size) AS size").
Where("status = ?", model.ResourceStatusReady)
if where != "" {
query = query.Where(where, args...)
}
query = query.Group(resourceProviderExpression() + ", endpoint, bucket, object_key")
var total struct {
Bytes int64 `gorm:"column:bytes"`
}
err := r.db.Table("(?) AS physical_resources", query).
Select("COALESCE(SUM(size), 0) AS bytes").
Scan(&total).Error
return total.Bytes, err
}
func (r *Repository) ResourceKindStats() ([]ResourceKindStat, error) {
var stats []ResourceKindStat
err := r.db.Model(&model.Resource{}).
Select("kind, COUNT(*) AS count, COALESCE(SUM(size), 0) AS logical_bytes").
Group("kind").
Order("logical_bytes desc, kind asc").
Scan(&stats).Error
if err != nil {
return nil, err
}
physical, err := r.resourcePhysicalStats("kind")
if err != nil {
return nil, err
}
for index := range stats {
stats[index].PhysicalBytes = physical[stats[index].Kind]
}
return stats, err
}
func (r *Repository) ResourceProviderStats() ([]ResourceProviderStat, error) {
var stats []ResourceProviderStat
provider := resourceProviderExpression()
err := r.db.Model(&model.Resource{}).
Select(provider + " AS provider, COUNT(*) AS count, COALESCE(SUM(size), 0) AS logical_bytes").
Group(provider).
Order("logical_bytes desc, provider asc").
Scan(&stats).Error
if err != nil {
return nil, err
}
physical, err := r.resourcePhysicalStats(provider)
if err != nil {
return nil, err
}
for index := range stats {
stats[index].PhysicalBytes = physical[stats[index].Provider]
}
return stats, err
}
func (r *Repository) resourcePhysicalStats(dimensionExpression string) (map[string]int64, error) {
query := r.db.Model(&model.Resource{}).
Select(dimensionExpression+" AS dimension, MAX(size) AS size").
Where("status = ?", model.ResourceStatusReady).
Group(dimensionExpression + ", " + resourceProviderExpression() + ", endpoint, bucket, object_key")
var rows []struct {
Dimension string `gorm:"column:dimension"`
Bytes int64 `gorm:"column:bytes"`
}
err := r.db.Table("(?) AS physical_resources", query).
Select("dimension, COALESCE(SUM(size), 0) AS bytes").
Group("dimension").
Scan(&rows).Error
result := make(map[string]int64, len(rows))
for _, row := range rows {
result[row.Dimension] = row.Bytes
}
return result, err
}
func resourceProviderExpression() string {
return "COALESCE(NULLIF(provider, ''), 'local')"
}