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
依赖注入的服务
- IBookmarkService: 书签业务逻辑服务
- IHttpClientFactory: HTTP 客户端工厂(用于下载远程图片)
- IObjectStorageOperation: 对象存储服务(如 OSS/S3)
- ILogger: 日志记录器
- IMapper: 对象映射器(Mapster)
- OwnerService: 所有权验证服务
- 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 注入图片上传委托函数(
UploadIcon和UploadImage)
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消息(解耦设计)
设计亮点
- 委托注入模式:将上传/删除逻辑通过委托注入到 ViewModel,实现职责分离
- 消息队列解耦:删除图片操作通过消息发布,避免阻塞主流程
- 自有资源检查:删除前通过
OwnerService检查是否为自有 CDN 资源 - 错误处理:图片下载失败时记录日志并返回 null
- RESTful 设计:标准的 HTTP 方法和状态码使用
- 权限控制:敏感操作需要 "System" 策略授权
注意事项
- 图片上传使用 ObjectId 生成唯一文件名,避免冲突
- 使用命名 HttpClient("edge")可能配置了特殊的代理或超时设置
- 删除操作使用消息队列,需要确保消费者正常运行
AI 正在分析代码…
评论加载中...