189 lines
6.3 KiB
Go
189 lines
6.3 KiB
Go
package app
|
|
|
|
import (
|
|
"errors"
|
|
"strings"
|
|
"time"
|
|
|
|
"infinite-canvas/backend/internal/model"
|
|
"infinite-canvas/backend/internal/repository"
|
|
|
|
"gorm.io/gorm"
|
|
)
|
|
|
|
type AdminResourceQuery struct {
|
|
Keyword string
|
|
Kind string
|
|
Status string
|
|
Provider string
|
|
UserID string
|
|
Page int
|
|
Limit int
|
|
}
|
|
|
|
type AdminStorageResourceView struct {
|
|
ID string `json:"id"`
|
|
UserID string `json:"userId"`
|
|
UserName string `json:"userName"`
|
|
Kind string `json:"kind"`
|
|
Status model.ResourceStatus `json:"status"`
|
|
Provider string `json:"provider"`
|
|
Bucket string `json:"bucket,omitempty"`
|
|
ObjectKey string `json:"objectKey"`
|
|
MimeType string `json:"mimeType"`
|
|
Size int64 `json:"size"`
|
|
PhysicalBytes int64 `json:"physicalBytes"`
|
|
Width int `json:"width"`
|
|
Height int `json:"height"`
|
|
DurationMs int64 `json:"durationMs"`
|
|
FileURL string `json:"fileUrl"`
|
|
CreatedAt time.Time `json:"createdAt"`
|
|
UpdatedAt time.Time `json:"updatedAt"`
|
|
}
|
|
|
|
type AdminResourcePage struct {
|
|
Items []AdminStorageResourceView `json:"items"`
|
|
Total int64 `json:"total"`
|
|
Page int `json:"page"`
|
|
Limit int `json:"pageSize"`
|
|
}
|
|
|
|
type AdminStorageStats struct {
|
|
repository.ResourceStorageSummary
|
|
ByKind []repository.ResourceKindStat `json:"byKind"`
|
|
ByProvider []repository.ResourceProviderStat `json:"byProvider"`
|
|
}
|
|
|
|
func (s *Service) AdminResourcePage(actor *model.User, query AdminResourceQuery) (*AdminResourcePage, error) {
|
|
if err := s.RequireAdmin(actor); err != nil {
|
|
return nil, err
|
|
}
|
|
filter, page, limit, err := normalizeAdminResourceQuery(query)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
resources, total, err := s.repo.AdminResources(filter)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
userIDs := make([]string, 0, len(resources))
|
|
seen := make(map[string]struct{}, len(resources))
|
|
for _, resource := range resources {
|
|
if _, exists := seen[resource.UserID]; exists {
|
|
continue
|
|
}
|
|
seen[resource.UserID] = struct{}{}
|
|
userIDs = append(userIDs, resource.UserID)
|
|
}
|
|
users, err := s.repo.UsersByIDs(userIDs)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
items := make([]AdminStorageResourceView, 0, len(resources))
|
|
for _, resource := range resources {
|
|
provider := normalizedResourceProvider(resource.Provider)
|
|
items = append(items, AdminStorageResourceView{
|
|
ID: resource.ID, UserID: resource.UserID, UserName: adminResourceUserName(users[resource.UserID]),
|
|
Kind: resource.Kind, Status: resource.Status, Provider: provider, Bucket: resource.Bucket,
|
|
ObjectKey: resource.ObjectKey, MimeType: resource.MimeType, Size: resource.Size,
|
|
PhysicalBytes: readyResourceBytes(resource), Width: resource.Width, Height: resource.Height, DurationMs: resource.DurationMs,
|
|
FileURL: "/api/admin/resources/" + resource.ID + "/file", CreatedAt: resource.CreatedAt, UpdatedAt: resource.UpdatedAt,
|
|
})
|
|
}
|
|
return &AdminResourcePage{Items: items, Total: total, Page: page, Limit: limit}, nil
|
|
}
|
|
|
|
func (s *Service) AdminStorageStats(actor *model.User) (*AdminStorageStats, error) {
|
|
if err := s.RequireAdmin(actor); err != nil {
|
|
return nil, err
|
|
}
|
|
summary, err := s.repo.ResourceStorageSummary()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
byKind, err := s.repo.ResourceKindStats()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
byProvider, err := s.repo.ResourceProviderStats()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return &AdminStorageStats{ResourceStorageSummary: summary, ByKind: byKind, ByProvider: byProvider}, nil
|
|
}
|
|
|
|
func (s *Service) OpenResourceRangeAsAdmin(actor *model.User, id string, rangeHeader string) (*ResourceStream, error) {
|
|
if err := s.RequireAdmin(actor); err != nil {
|
|
return nil, err
|
|
}
|
|
resource, err := s.repo.Resource(strings.TrimSpace(id))
|
|
if err != nil {
|
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
|
return nil, NotFound("资源不存在")
|
|
}
|
|
return nil, err
|
|
}
|
|
resource.Provider = normalizedResourceProvider(resource.Provider)
|
|
return s.openResourceRange(resource.UserID, resource, rangeHeader)
|
|
}
|
|
|
|
func normalizeAdminResourceQuery(query AdminResourceQuery) (repository.AdminResourceFilter, int, int, error) {
|
|
page, limit := normalizeAdminPage(query.Page, query.Limit)
|
|
filter := repository.AdminResourceFilter{
|
|
Keyword: strings.TrimSpace(query.Keyword),
|
|
Kind: strings.ToLower(strings.TrimSpace(query.Kind)),
|
|
Status: strings.ToLower(strings.TrimSpace(query.Status)),
|
|
Provider: strings.ToLower(strings.TrimSpace(query.Provider)),
|
|
UserID: strings.TrimSpace(query.UserID),
|
|
Limit: limit,
|
|
Offset: (page - 1) * limit,
|
|
}
|
|
if filter.Kind != "" && !oneOf(filter.Kind, "image", "video", "audio", "file") {
|
|
return repository.AdminResourceFilter{}, 0, 0, BadAuthRequest("资源类型筛选无效")
|
|
}
|
|
if filter.Status != "" && !oneOf(filter.Status, string(model.ResourceStatusPending), string(model.ResourceStatusReady), string(model.ResourceStatusFailed), string(model.ResourceStatusDeleted)) {
|
|
return repository.AdminResourceFilter{}, 0, 0, BadAuthRequest("资源状态筛选无效")
|
|
}
|
|
if filter.Provider != "" && !oneOf(filter.Provider, "local", aliyunOSSProvider, tencentCOSProvider, qiniuKodoProvider, s3Provider) {
|
|
return repository.AdminResourceFilter{}, 0, 0, BadAuthRequest("资源存储位置筛选无效")
|
|
}
|
|
if len(filter.Keyword) > 200 || len(filter.UserID) > 64 {
|
|
return repository.AdminResourceFilter{}, 0, 0, BadAuthRequest("资源筛选条件过长")
|
|
}
|
|
return filter, page, limit, nil
|
|
}
|
|
|
|
func normalizedResourceProvider(provider string) string {
|
|
provider = strings.ToLower(strings.TrimSpace(provider))
|
|
if provider == "" {
|
|
return "local"
|
|
}
|
|
return provider
|
|
}
|
|
|
|
func readyResourceBytes(resource model.Resource) int64 {
|
|
if resource.Status != model.ResourceStatusReady {
|
|
return 0
|
|
}
|
|
return resource.Size
|
|
}
|
|
|
|
func adminResourceUserName(user model.User) string {
|
|
if value := strings.TrimSpace(user.DisplayName); value != "" {
|
|
return value
|
|
}
|
|
if value := strings.TrimSpace(user.Username); value != "" {
|
|
return value
|
|
}
|
|
return user.ID
|
|
}
|
|
|
|
func oneOf(value string, allowed ...string) bool {
|
|
for _, item := range allowed {
|
|
if value == item {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|