媒体资产管理Skill media-asset-management

媒体资产管理技能用于设计数字资产管理系统、媒体库和上传管道,涵盖媒体存储模式、文件组织、元数据提取和用于无头CMS的媒体API。关键词包括数字资产管理、媒体库、上传管道、元数据、存储架构、API开发,适用于软件开发中的系统设计。

架构设计 0 次安装 0 次浏览 更新于 3/11/2026

名称: 媒体资产管理 描述: 当设计数字资产管理系统、媒体库、上传管道或资产元数据模式时使用。涵盖媒体存储模式、文件组织、元数据提取以及用于无头CMS的媒体API。 允许的工具: 阅读, Glob, Grep, 任务, 技能

媒体资产管理

为无头CMS设计数字资产管理系统、媒体库和上传管道的指南。

何时使用此技能

  • 设计媒体库架构
  • 实现文件上传管道
  • 规划资产元数据模式
  • 配置存储提供商
  • 构建媒体搜索和筛选

媒体资产模型

核心实体

public class MediaItem
{
    public Guid Id { get; set; }

    // 文件信息
    public string FileName { get; set; } = string.Empty;
    public string Extension { get; set; } = string.Empty;
    public string MimeType { get; set; } = string.Empty;
    public long SizeBytes { get; set; }

    // 存储
    public string StorageProvider { get; set; } = string.Empty;
    public string StoragePath { get; set; } = string.Empty;
    public string PublicUrl { get; set; } = string.Empty;

    // 组织
    public Guid? FolderId { get; set; }
    public MediaFolder? Folder { get; set; }
    public List<string> Tags { get; set; } = new();

    // 元数据
    public MediaMetadata Metadata { get; set; } = new();

    // 审计
    public string UploadedBy { get; set; } = string.Empty;
    public DateTime UploadedUtc { get; set; }
    public DateTime? ModifiedUtc { get; set; }
}

public class MediaMetadata
{
    // 通用
    public string? Title { get; set; }
    public string? Description { get; set; }
    public string? Alt { get; set; }
    public string? Caption { get; set; }
    public string? Credit { get; set; }

    // 图像特定
    public int? Width { get; set; }
    public int? Height { get; set; }
    public string? ColorSpace { get; set; }

    // 文档特定
    public int? PageCount { get; set; }
    public string? Author { get; set; }

    // 视频特定
    public TimeSpan? Duration { get; set; }
    public string? Codec { get; set; }
    public int? Bitrate { get; set; }

    // EXIF/XMP
    public Dictionary<string, string> ExifData { get; set; } = new();
}

public class MediaFolder
{
    public Guid Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public string Path { get; set; } = string.Empty;
    public Guid? ParentId { get; set; }
    public List<MediaFolder> Children { get; set; } = new();
}

存储架构

存储提供商抽象

public interface IMediaStorageProvider
{
    string ProviderName { get; }

    Task<string> UploadAsync(Stream stream, string path, string contentType);
    Task<Stream> DownloadAsync(string path);
    Task DeleteAsync(string path);
    Task<bool> ExistsAsync(string path);
    string GetPublicUrl(string path);
}

// Azure Blob 存储
public class AzureBlobStorageProvider : IMediaStorageProvider
{
    public string ProviderName => "AzureBlob";

    public async Task<string> UploadAsync(
        Stream stream, string path, string contentType)
    {
        var blobClient = _containerClient.GetBlobClient(path);

        await blobClient.UploadAsync(stream, new BlobHttpHeaders
        {
            ContentType = contentType,
            CacheControl = "public, max-age=31536000"
        });

        return path;
    }

    public string GetPublicUrl(string path)
    {
        return $"{_containerClient.Uri}/{path}";
    }
}

// AWS S3
public class S3StorageProvider : IMediaStorageProvider
{
    public string ProviderName => "S3";

    public async Task<string> UploadAsync(
        Stream stream, string path, string contentType)
    {
        var request = new PutObjectRequest
        {
            BucketName = _bucketName,
            Key = path,
            InputStream = stream,
            ContentType = contentType,
            CannedACL = S3CannedACL.PublicRead
        };

        await _s3Client.PutObjectAsync(request);
        return path;
    }
}

// 本地文件系统
public class LocalStorageProvider : IMediaStorageProvider
{
    public string ProviderName => "Local";

    public async Task<string> UploadAsync(
        Stream stream, string path, string contentType)
    {
        var fullPath = Path.Combine(_basePath, path);
        Directory.CreateDirectory(Path.GetDirectoryName(fullPath)!);

        await using var fileStream = File.Create(fullPath);
        await stream.CopyToAsync(fileStream);

        return path;
    }
}

路径生成

public class MediaPathGenerator
{
    public string GeneratePath(string fileName, PathStrategy strategy)
    {
        var ext = Path.GetExtension(fileName);
        var name = Path.GetFileNameWithoutExtension(fileName);
        var safeName = Slugify(name);

        return strategy switch
        {
            PathStrategy.DateBased => $"{DateTime.UtcNow:yyyy/MM/dd}/{safeName}-{Guid.NewGuid():N}{ext}",
            PathStrategy.HashBased => $"{ComputeHash(fileName)[..2]}/{ComputeHash(fileName)[2..4]}/{Guid.NewGuid():N}{ext}",
            PathStrategy.Flat => $"{Guid.NewGuid():N}{ext}",
            PathStrategy.OriginalName => $"{safeName}-{DateTime.UtcNow:yyyyMMddHHmmss}{ext}",
            _ => throw new ArgumentOutOfRangeException()
        };
    }
}

public enum PathStrategy
{
    DateBased,      // 2025/01/15/image-abc123.jpg
    HashBased,      // ab/cd/abc123.jpg
    Flat,           // abc123.jpg
    OriginalName    // my-image-20250115103045.jpg
}

上传管道

上传服务

public class MediaUploadService
{
    public async Task<MediaItem> UploadAsync(
        Stream stream,
        string fileName,
        string contentType,
        UploadOptions? options = null)
    {
        options ??= new UploadOptions();

        // 验证
        ValidateFile(fileName, contentType, stream.Length, options);

        // 生成路径
        var path = _pathGenerator.GeneratePath(fileName, options.PathStrategy);

        // 处理(调整大小、优化)
        var processedStream = await ProcessMediaAsync(stream, contentType, options);

        // 上传到存储
        var storagePath = await _storageProvider.UploadAsync(
            processedStream, path, contentType);

        // 提取元数据
        var metadata = await ExtractMetadataAsync(processedStream, contentType);

        // 创建记录
        var mediaItem = new MediaItem
        {
            Id = Guid.NewGuid(),
            FileName = fileName,
            Extension = Path.GetExtension(fileName),
            MimeType = contentType,
            SizeBytes = processedStream.Length,
            StorageProvider = _storageProvider.ProviderName,
            StoragePath = storagePath,
            PublicUrl = _storageProvider.GetPublicUrl(storagePath),
            FolderId = options.FolderId,
            Tags = options.Tags ?? new List<string>(),
            Metadata = metadata,
            UploadedBy = _currentUser.UserId,
            UploadedUtc = DateTime.UtcNow
        };

        await _repository.AddAsync(mediaItem);

        // 触发事件
        await _mediator.Publish(new MediaUploadedEvent(mediaItem));

        return mediaItem;
    }

    private void ValidateFile(
        string fileName, string contentType, long size, UploadOptions options)
    {
        // 检查文件大小
        if (size > options.MaxFileSizeBytes)
            throw new MediaValidationException($"文件大小超过最大限制 {options.MaxFileSizeBytes} 字节");

        // 检查允许的类型
        if (options.AllowedMimeTypes?.Any() == true &&
            !options.AllowedMimeTypes.Contains(contentType))
            throw new MediaValidationException($"文件类型 {contentType} 不被允许");

        // 检查扩展名
        var ext = Path.GetExtension(fileName).ToLowerInvariant();
        if (options.BlockedExtensions?.Contains(ext) == true)
            throw new MediaValidationException($"文件扩展名 {ext} 被阻止");
    }
}

public class UploadOptions
{
    public Guid? FolderId { get; set; }
    public List<string>? Tags { get; set; }
    public PathStrategy PathStrategy { get; set; } = PathStrategy.DateBased;
    public long MaxFileSizeBytes { get; set; } = 10 * 1024 * 1024; // 10MB
    public List<string>? AllowedMimeTypes { get; set; }
    public List<string>? BlockedExtensions { get; set; }
    public bool ExtractMetadata { get; set; } = true;
    public ImageProcessingOptions? ImageOptions { get; set; }
}

元数据提取

public class MetadataExtractor
{
    public async Task<MediaMetadata> ExtractAsync(Stream stream, string contentType)
    {
        var metadata = new MediaMetadata();

        if (contentType.StartsWith("image/"))
        {
            await ExtractImageMetadataAsync(stream, metadata);
        }
        else if (contentType.StartsWith("video/"))
        {
            await ExtractVideoMetadataAsync(stream, metadata);
        }
        else if (contentType == "application/pdf")
        {
            await ExtractPdfMetadataAsync(stream, metadata);
        }

        return metadata;
    }

    private async Task ExtractImageMetadataAsync(Stream stream, MediaMetadata metadata)
    {
        using var image = await Image.LoadAsync(stream);

        metadata.Width = image.Width;
        metadata.Height = image.Height;

        // 提取 EXIF
        if (image.Metadata.ExifProfile != null)
        {
            foreach (var value in image.Metadata.ExifProfile.Values)
            {
                metadata.ExifData[value.Tag.ToString()] = value.GetValue()?.ToString() ?? "";
            }
        }
    }
}

媒体库功能

文件夹管理

public class MediaFolderService
{
    public async Task<MediaFolder> CreateFolderAsync(string name, Guid? parentId = null)
    {
        var folder = new MediaFolder
        {
            Id = Guid.NewGuid(),
            Name = name,
            ParentId = parentId,
            Path = await BuildPathAsync(name, parentId)
        };

        await _repository.AddAsync(folder);
        return folder;
    }

    public async Task<List<MediaFolder>> GetFolderTreeAsync()
    {
        var folders = await _repository.GetAllAsync();
        return BuildTree(folders.Where(f => f.ParentId == null));
    }
}

媒体搜索

public class MediaSearchService
{
    public async Task<PagedResult<MediaItem>> SearchAsync(MediaSearchQuery query)
    {
        var queryable = _context.MediaItems.AsQueryable();

        // 按文件夹筛选
        if (query.FolderId.HasValue)
        {
            queryable = queryable.Where(m => m.FolderId == query.FolderId);
        }

        // 按类型筛选
        if (!string.IsNullOrEmpty(query.MediaType))
        {
            queryable = query.MediaType switch
            {
                "image" => queryable.Where(m => m.MimeType.StartsWith("image/")),
                "video" => queryable.Where(m => m.MimeType.StartsWith("video/")),
                "document" => queryable.Where(m =>
                    m.MimeType == "application/pdf" ||
                    m.MimeType.Contains("document")),
                _ => queryable
            };
        }

        // 按标签筛选
        if (query.Tags?.Any() == true)
        {
            queryable = queryable.Where(m =>
                query.Tags.All(t => m.Tags.Contains(t)));
        }

        // 搜索文本
        if (!string.IsNullOrEmpty(query.SearchText))
        {
            var search = query.SearchText.ToLower();
            queryable = queryable.Where(m =>
                m.FileName.ToLower().Contains(search) ||
                m.Metadata.Title!.ToLower().Contains(search) ||
                m.Metadata.Description!.ToLower().Contains(search));
        }

        // 应用排序
        queryable = query.SortBy switch
        {
            "name" => queryable.OrderBy(m => m.FileName),
            "date" => queryable.OrderByDescending(m => m.UploadedUtc),
            "size" => queryable.OrderByDescending(m => m.SizeBytes),
            _ => queryable.OrderByDescending(m => m.UploadedUtc)
        };

        return await queryable.ToPagedResultAsync(query.Page, query.PageSize);
    }
}

public class MediaSearchQuery
{
    public Guid? FolderId { get; set; }
    public string? MediaType { get; set; }
    public List<string>? Tags { get; set; }
    public string? SearchText { get; set; }
    public string? SortBy { get; set; }
    public int Page { get; set; } = 1;
    public int PageSize { get; set; } = 20;
}

媒体 API

端点

POST   /api/media/upload              # 上传单个文件
POST   /api/media/upload/bulk         # 批量上传
GET    /api/media                     # 列表/搜索媒体
GET    /api/media/{id}                # 获取媒体项
DELETE /api/media/{id}                # 删除媒体
PATCH  /api/media/{id}                # 更新元数据

# 文件夹
GET    /api/media/folders             # 获取文件夹树
POST   /api/media/folders             # 创建文件夹
DELETE /api/media/folders/{id}        # 删除文件夹

媒体响应

{
  "data": {
    "id": "media-123",
    "fileName": "hero-image.jpg",
    "mimeType": "image/jpeg",
    "sizeBytes": 245678,
    "url": "https://cdn.example.com/media/2025/01/15/hero-image-abc123.jpg",
    "metadata": {
      "title": "主页英雄图",
      "alt": "团队合作",
      "width": 1920,
      "height": 1080
    },
    "folder": {
      "id": "folder-456",
      "name": "主页",
      "path": "/营销/主页"
    },
    "tags": ["英雄", "主页", "团队"],
    "uploadedBy": "用户-789",
    "uploadedUtc": "2025-01-15T10:30:00Z"
  }
}

相关技能

  • image-optimization - 图像处理和优化
  • cdn-media-delivery - CDN配置和交付
  • content-type-modeling - 内容类型中的媒体字段