using Dpz.Core.Entity.Base.Image;
using Dpz.Core.Infrastructure.Imaging;
using Dpz.Core.Public.ViewModel.Request;
using Dpz.Core.Public.ViewModel.Response;

namespace Dpz.Core.WebApi.Controllers;

/// <summary>
/// 图片管理
/// </summary>
[ApiController, Route("api/[controller]")]
public class PictureController(
    IPictureRecordService pictureRecordService,
    IObjectStorageOperation objectStorageService,
    ILogger<PictureController> logger,
    IHttpCurrentUserService currentUserService,
    IImageFormatDetector imageFormatDetector
) : ControllerBase
{
    /// <summary>
    /// 图片列表
    /// </summary>
    /// <returns></returns>
    [HttpGet]
    [ProducesResponseType<List<PictureRecordResponse>>(StatusCodes.Status200OK)]
    public async Task<IActionResult> GetPictures([FromQuery] PictureQueryParameterDto parameter)
    {
        var list = await pictureRecordService.GetPagesAsync(
            parameter.Tag == null ? null : [parameter.Tag],
            parameter.Description,
            parameter.PageIndex,
            parameter.PageSize
        );
        list.AddPaginationMetadata(Response.Headers);
        return Ok(list);
    }

    /// <summary>
    /// 上传图片
    /// </summary>
    /// <returns></returns>
    [HttpPost]
    [Authorize(Policy = "System")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(StatusCodes.Status400BadRequest)]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    public async Task<IActionResult> CreatePicture([FromForm] PictureCreateDto model)
    {
        if (model.Image.Length <= 0 || !model.Image.ContentType.Contains("image"))
        {
            return BadRequest("please select image to upload");
        }

        model.Tags = model.Tags.Select(x => x.Trim()).Where(x => !string.IsNullOrEmpty(x)).ToList();
        var userInfo = await currentUserService.GetRequiredUserInfoAsync();

        using var memoryStream = new MemoryStream();
        await model.Image.CopyToAsync(memoryStream);
        var bytes = memoryStream.ToArray();

        var path = new[] { "images", "album", DateTime.Now.ToString("yyyy-MM") };
        var filename = $"{ObjectId.GenerateNewId()}.{GetImageFormat(bytes) ?? "jpg"}";
        var result = await objectStorageService.UploadAsync(
            model.Image.OpenReadStream(),
            path,
            filename
        );
        if (result is not ImageMetadata imageMetadata)
        {
            return BadRequest("image metadata not found after upload");
        }

        var createRequest = new CreatePictureRecordRequest
        {
            Creator = userInfo,
            UploadTime = DateTime.Now,
            Tags = model.Tags,
            Description = model.Description,
            Category = PictureCategory.Album,
            AccessUrl = imageMetadata.Url,
            Length = imageMetadata.Size,
            ObjectStorageUploadTime = DateTime.Now,
            Md5 = imageMetadata.Hash ?? CalculateMd5(bytes),
            Width = imageMetadata.Width,
            Height = imageMetadata.Height,
        };
        await pictureRecordService.CreateRecordAsync(createRequest);

        return NoContent();
    }

    [NonAction]
    private static string CalculateMd5(byte[] bytes)
    {
        using var md5 = MD5.Create();
        var hash = md5.ComputeHash(bytes);
        return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
    }

    [NonAction]
    private string? GetImageFormat(byte[] bytes)
    {
        try
        {
            var info = imageFormatDetector.Detect(bytes);
            return info.IsKnown ? info.DefaultExtension : null;
        }
        catch (Exception e)
        {
            logger.LogError(e, "get image format fail");
            return null;
        }
    }

    /// <summary>
    /// 修改图像、图像信息
    /// </summary>
    /// <returns></returns>
    [HttpPatch]
    [Authorize(Policy = "System")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    public async Task<IActionResult> EditPicture([FromBody] PictureEditDto model)
    {
        model.Tags = model.Tags.Select(x => x.Trim()).Where(x => !string.IsNullOrEmpty(x)).ToList();

        var recordChange = new EditPictureRecordRequest
        {
            Category = PictureCategory.Album,
            Description = model.Description,
            Tags = model.Tags,
            Id = model.Id,
        };

        await pictureRecordService.ChangeInformationAsync(recordChange);
        return NoContent();
    }

    /// <summary>
    /// 获取图片信息
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    [HttpGet("{id}")]
    [ProducesResponseType<PictureRecordResponse>(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> GetPicture(string id)
    {
        var picture = await pictureRecordService.GetPictureRecordAsync(id);
        if (picture == null)
        {
            return NotFound();
        }
        return Ok(picture);
    }

    /// <summary>
    /// 删除图像
    /// </summary>
    /// <param name="id"></param>
    /// <returns></returns>
    [HttpDelete("{id}")]
    [Authorize(Policy = "System")]
    [ProducesResponseType(StatusCodes.Status204NoContent)]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    [ProducesResponseType(StatusCodes.Status404NotFound)]
    public async Task<IActionResult> Delete(string id)
    {
        var pictureRecord = await pictureRecordService.GetPictureRecordAsync(id);
        if (pictureRecord == null)
        {
            return NotFound();
        }
        await pictureRecordService.DeleteAsync(id);
        await objectStorageService.DeleteAsync(pictureRecord.AccessUrl);
        return NoContent();
    }

    /// <summary>
    /// 获取所有标签
    /// </summary>
    /// <returns></returns>
    [HttpGet("tags")]
    [ProducesResponseType<List<string>>(StatusCodes.Status200OK)]
    [ProducesResponseType(StatusCodes.Status401Unauthorized)]
    public async Task<IActionResult> GetPictureTags()
    {
        var tags = await pictureRecordService.GetTagsAsync();
        return Ok(tags);
    }
}
⚠⚠    以下内容为AI分析的结果,请根据实际情况进行判断。

代码解释

这是一个基于 ASP.NET Core 的图片管理 WebAPI 控制器,提供了完整的图片 CRUD(增删改查)功能。

主要功能模块

1. 依赖注入的服务

控制器通过主构造函数注入了以下服务:

  • IPictureRecordService: 图片记录数据服务
  • IObjectStorageOperation: 对象存储服务(用于上传/删除图片文件)
  • ILogger: 日志服务
  • IHttpCurrentUserService: 当前用户信息服务
  • IImageFormatDetector: 图片格式检测服务

2. API 端点详解

GET /api/picture - 获取图片列表

  • 支持分页查询
  • 可按标签(Tag)、描述(Description)筛选
  • 返回分页元数据到响应头

POST /api/picture - 上传图片

  • 🔒 需要 "System" 权限认证
  • 验证文件类型必须是图片
  • 处理流程:
    1. 清理和验证标签数据
    2. 读取图片文件到内存流
    3. 生成唯一文件名(使用 ObjectId)
    4. 上传到对象存储(按年月组织目录结构)
    5. 提取图片元数据(宽高、大小、Hash等)
    6. 创建数据库记录

PATCH /api/picture - 修改图片信息

  • 🔒 需要 "System" 权限认证
  • 只能修改描述和标签,不能修改图片本身

GET /api/picture/{id} - 获取单张图片信息

  • 根据 ID 查询图片详情
  • 不存在返回 404

DELETE /api/picture/{id} - 删除图片

  • 🔒 需要 "System" 权限认证
  • 同时删除:
    1. 数据库记录
    2. 对象存储中的文件

GET /api/picture/tags - 获取所有标签

  • 返回系统中所有已使用的图片标签列表

3. 辅助方法

CalculateMd5(byte[] bytes)

  • 计算文件的 MD5 哈希值
  • 用于文件去重和完整性校验

GetImageFormat(byte[] bytes)

  • 检测图片格式(jpg、png等)
  • 异常处理后返回 null(防止格式检测失败)

技术特点

  1. 安全性: 使用 [Authorize] 特性保护敏感操作
  2. 文档化: 使用 XML 注释和 ProducesResponseType 生成 Swagger 文档
  3. 错误处理: 完善的验证和错误响应(400/401/404)
  4. 存储组织: 按时间(年-月)组织文件存储路径
  5. 元数据管理: 记录图片的完整元数据(尺寸、Hash、上传时间等)
  6. 标签系统: 支持多标签分类和查询

数据流向

客户端 → 控制器 → 业务服务层 → 对象存储/数据库
                          ↓
                    返回响应(JSON/NoContent)

这是一个设计良好、职责清晰的 RESTful API 控制器实现。

评论加载中...