using Dpz.Core.MessageQueue.Abstractions;
using Dpz.Core.Public.ViewModel.Messages;
using MapsterMapper;

namespace Dpz.Core.WebApi.Controllers;

/// <summary>
/// 书签
/// </summary>
[ApiController, Route("api/[controller]")]
public class BookmarkController(
    IBookmarkService bookmarkService,
    IHttpClientFactory httpClientFactory,
    IObjectStorageOperation objectStorageService,
    ILogger<BookmarkController> logger,
    IMapper mapper,
    OwnerService ownerService,
    IMessagePublisher<RemoveImagesMessage> removeImageMessagePublisher
) : ControllerBase
{
    /// <summary>
    /// 获取书签
    /// </summary>
    /// <param name="title">标题</param>
    /// <param name="categories">分类</param>
    /// <returns></returns>
    [HttpGet]
    [ProducesResponseType<List<VmBookmark>>(StatusCodes.Status200OK)]
    public async Task<IActionResult> GetBookmarks(
        [FromQuery] string title = "",
        [FromQuery] string[]? categories = null
    )
    {
        var list = await bookmarkService.GetBookmarksAsync(title, categories);
        return Ok(list);
    }

    /// <summary>
    /// 获取所有分类
    /// </summary>
    /// <returns></returns>
    [HttpGet("categories")]
    [ProducesResponseType<List<string>>(StatusCodes.Status200OK)]
    public async Task<IActionResult> GetAllCategories()
    {
        var list = await bookmarkService.GetCategoriesAsync();
        return Ok(list);
    }

    /// <summary>
    /// 搜索书签
    /// </summary>
    /// <param name="title">标题</param>
    /// <param name="categories">分类</param>
    /// <returns></returns>
    [HttpGet("search")]
    [ProducesResponseType<List<string>>(StatusCodes.Status200OK)]
    public async Task<IActionResult> Search(
        [FromQuery] string title = "",
        [FromQuery] string[]? categories = null
    )
    {
        var list = await bookmarkService.SearchAsync(title, categories);
        return Ok(list);
    }

    /// <summary>
    /// 书签详情
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    [HttpGet("{id}")]
    [ProducesResponseType<VmBookmark>(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> GetBookmark(string id)
    {
        var bookmark = await bookmarkService.GetBookmarkAsync(id);
        if (bookmark == null)
        {
            return NotFound();
        }

        return Ok(bookmark);
    }

    /// <summary>
    /// 新建书签
    /// </summary>
    /// <param name="bookmark"></param>
    /// <returns></returns>
    [HttpPost, Authorize(Policy = "System")]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    public async Task<IActionResult> CreateBookmark(CreateBookmarkDto bookmark)
    {
        var viewModel = mapper.Map<VmCreateBookmark>(bookmark);
        viewModel.UploadIcon = UploadIconAsync;
        viewModel.UploadImage = UploadImageAsync;
        await bookmarkService.CreateBookmarkAsync(viewModel);
        return NoContent();
    }

    /// <summary>
    /// 删除书签
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    [HttpDelete("{id}"), Authorize(nameof(Permissions.System))]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    public async Task<IActionResult> Delete(string id)
    {
        await bookmarkService.DeleteAsync(
            id,
            async x =>
            {
                if (ownerService.IsOwnCdnImage(x.Icon))
                {
                    await objectStorageService.DeleteAsync(x.Icon);
                }

                if (ownerService.IsOwnCdnImage(x.Image))
                {
                    await objectStorageService.DeleteAsync(x.Image);
                }
            }
        );
        return NoContent();
    }

    /// <summary>
    /// 更新标签
    /// </summary>
    /// <param name="bookmark"></param>
    /// <returns></returns>
    [HttpPut, Authorize(Policy = "System")]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    public async Task<IActionResult> UpdateBookmark(UpdateBookmarkDto bookmark)
    {
        var viewModel = mapper.Map<VmUpdateBookmark>(bookmark);
        viewModel.UploadIcon = UploadIconAsync;
        viewModel.UploadImage = UploadImageAsync;
        viewModel.DeleteIcon = DeleteObjectStorageAsync;
        viewModel.DeleteImage = DeleteObjectStorageAsync;
        await bookmarkService.UpdateAsync(viewModel);
        return NoContent();
    }

    [NonAction]
    async Task<string?> UploadIconAsync(string url)
    {
        if (string.IsNullOrEmpty(url))
        {
            return null;
        }

        var uri = new Uri(url);
        var extensionName = Path.GetExtension(uri.AbsolutePath);
        var httpClient = httpClientFactory.CreateClient("edge");
        var request = new HttpRequestMessage(HttpMethod.Get, uri);
        try
        {
            var response = await httpClient.SendAsync(request);
            var stream = await response.Content.ReadAsStreamAsync();
            var result = await objectStorageService.UploadAsync(
                stream,
                ["images", "bookmark", "icon"],
                $"{ObjectId.GenerateNewId()}{extensionName}"
            );
            return result.Url;
        }
        catch (Exception e)
        {
            logger.LogError(e, "download icon fail");
            return null;
        }
    }

    [NonAction]
    async Task<string?> UploadImageAsync(string url)
    {
        if (string.IsNullOrEmpty(url))
        {
            return null;
        }

        var uri = new Uri(url);
        var extensionName = Path.GetExtension(uri.AbsolutePath);
        var httpClient = httpClientFactory.CreateClient("edge");
        var request = new HttpRequestMessage(HttpMethod.Get, uri);
        try
        {
            var response = await httpClient.SendAsync(request);
            var stream = await response.Content.ReadAsStreamAsync();
            var result = await objectStorageService.UploadAsync(
                stream,
                ["images", "bookmark", "image"],
                $"{ObjectId.GenerateNewId()}{extensionName}"
            );
            return result.Url;
        }
        catch (Exception e)
        {
            logger.LogError(e, "download image fail");
            return null;
        }
    }

    [NonAction]
    async Task DeleteObjectStorageAsync(string url)
    {
        await removeImageMessagePublisher.PublishAsync(new RemoveImagesMessage { ImageUrl = url });
    }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

代码解释

这是一个 ASP.NET Core Web API 控制器,用于管理**书签(Bookmark)**功能。以下是详细解释:

整体架构

控制器定义

[ApiController, Route("api/[controller]")]
public class BookmarkController : ControllerBase
  • 使用主构造函数(C# 12 特性)注入7个依赖项
  • 路由为 api/Bookmark

依赖注入的服务

  1. IBookmarkService: 书签业务逻辑服务
  2. IHttpClientFactory: HTTP 客户端工厂(用于下载远程图片)
  3. IObjectStorageOperation: 对象存储服务(如 OSS/S3)
  4. ILogger: 日志记录器
  5. IMapper: 对象映射器(Mapster)
  6. OwnerService: 所有权验证服务
  7. IMessagePublisher: 消息发布器(用于异步删除图片)

API 端点详解

1. GET /api/Bookmark - 获取书签列表

GetBookmarks(string title, string[]? categories)
  • 支持按标题和分类筛选
  • 返回 List<VmBookmark>

2. GET /api/Bookmark/categories - 获取所有分类

GetAllCategories()
  • 返回所有书签分类的列表

3. GET /api/Bookmark/search - 搜索书签

Search(string title, string[]? categories)
  • GetBookmarks 功能类似,可能有不同的搜索逻辑

4. GET /api/Bookmark/{id} - 获取单个书签详情

GetBookmark(string id)
  • 返回 404 如果书签不存在

5. POST /api/Bookmark - 创建书签

CreateBookmark(CreateBookmarkDto bookmark)
  • 需要授权Authorize(Policy = "System")
  • 使用 Mapster 映射 DTO 到 ViewModel
  • 为 ViewModel 注入图片上传委托函数UploadIconUploadImage

6. DELETE /api/Bookmark/{id} - 删除书签

Delete(string id)
  • 需要授权Authorize(nameof(Permissions.System))
  • 删除前检查图标和图片是否为自有 CDN 资源
  • 如果是,则从对象存储中删除

7. PUT /api/Bookmark - 更新书签

UpdateBookmark(UpdateBookmarkDto bookmark)
  • 同样注入上传和删除委托函数
  • 支持更新图标和图片

核心辅助方法

1. UploadIconAsync - 上传图标

async Task<string?> UploadIconAsync(string url)
  • 从远程 URL 下载图标
  • 使用 HttpClient(名称为 "edge")获取图片流
  • 生成新的 ObjectId 作为文件名
  • 上传到对象存储的 images/bookmark/icon 路径
  • 返回上传后的 URL

2. UploadImageAsync - 上传封面图片

async Task<string?> UploadImageAsync(string url)
  • 逻辑与 UploadIconAsync 相同
  • 上传到 images/bookmark/image 路径

3. DeleteObjectStorageAsync - 删除对象存储资源

async Task DeleteObjectStorageAsync(string url)
  • 通过消息队列异步删除图片
  • 发布 RemoveImagesMessage 消息(解耦设计)

设计亮点

  1. 委托注入模式:将上传/删除逻辑通过委托注入到 ViewModel,实现职责分离
  2. 消息队列解耦:删除图片操作通过消息发布,避免阻塞主流程
  3. 自有资源检查:删除前通过 OwnerService 检查是否为自有 CDN 资源
  4. 错误处理:图片下载失败时记录日志并返回 null
  5. RESTful 设计:标准的 HTTP 方法和状态码使用
  6. 权限控制:敏感操作需要 "System" 策略授权

注意事项

  • 图片上传使用 ObjectId 生成唯一文件名,避免冲突
  • 使用命名 HttpClient("edge")可能配置了特殊的代理或超时设置
  • 删除操作使用消息队列,需要确保消费者正常运行
评论加载中...