PictureAppService.cs 3.73 KB
using FoodLabeling.Application.Contracts.Dtos.Picture;
using FoodLabeling.Application.Contracts.IServices;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Hosting;
using Volo.Abp;
using Volo.Abp.Application.Services;
using Volo.Abp.Guids;

namespace FoodLabeling.Application.Services;

public class PictureAppService : ApplicationService, IPictureAppService
{
    private const long MaxSizeBytes = 5 * 1024 * 1024;
    private static readonly HashSet<string> AllowedExtensions = new(StringComparer.OrdinalIgnoreCase)
    {
        ".jpg",
        ".jpeg",
        ".png",
        ".webp",
        ".gif"
    };

    private readonly IGuidGenerator _guidGenerator;
    private readonly IHostEnvironment _hostEnvironment;

    public PictureAppService(IGuidGenerator guidGenerator, IHostEnvironment hostEnvironment)
    {
        _guidGenerator = guidGenerator;
        _hostEnvironment = hostEnvironment;
    }

    /// <summary>
    /// 上传类别图片(保存到 /www/wwwroot/FoodLabelingManagementUs/picture)
    /// </summary>
    /// <remarks>返回的 Url 可直接保存到 CategoryPhotoUrl。</remarks>
    [HttpPost]
    [Consumes("multipart/form-data")]
    [Route("/api/app/picture/category/upload")]
    public async Task<PictureUploadOutputDto> UploadCategoryAsync([FromForm] PictureUploadInputVo input)
    {
        if (input.File is null || input.File.Length <= 0)
        {
            throw new UserFriendlyException("请选择要上传的图片文件");
        }

        if (input.File.Length > MaxSizeBytes)
        {
            throw new UserFriendlyException("图片大小不能超过5MB");
        }

        var ext = Path.GetExtension(input.File.FileName ?? string.Empty);
        if (string.IsNullOrWhiteSpace(ext) || !AllowedExtensions.Contains(ext))
        {
            throw new UserFriendlyException("仅支持上传 jpg/jpeg/png/webp/gif 格式图片");
        }

        var subDir = NormalizeSubDir(input.SubDir);
        var saveRoot = ResolvePictureRoot();
        var saveDir = string.IsNullOrWhiteSpace(subDir) ? saveRoot : Path.Combine(saveRoot, subDir);
        Directory.CreateDirectory(saveDir);

        var fileName = $"{DateTime.Now:yyyyMMddHHmmss}_{_guidGenerator.Create():N}{ext.ToLowerInvariant()}";
        var savePath = Path.Combine(saveDir, fileName);

        await using (var stream = new FileStream(savePath, FileMode.CreateNew, FileAccess.Write, FileShare.None))
        {
            await input.File.CopyToAsync(stream);
        }

        var url = BuildPictureUrl(subDir, fileName);
        return new PictureUploadOutputDto
        {
            Url = url,
            FileName = fileName,
            Size = input.File.Length
        };
    }

    private string ResolvePictureRoot()
    {
        var linuxPictureRoot = "/www/wwwroot/FoodLabelingManagementUs/picture";
        var webRootPicture = Path.Combine(_hostEnvironment.ContentRootPath, "wwwroot", "FoodLabelingManagementUs", "picture");
        return Directory.Exists(linuxPictureRoot) ? linuxPictureRoot : webRootPicture;
    }

    private static string NormalizeSubDir(string? subDir)
    {
        if (string.IsNullOrWhiteSpace(subDir))
        {
            return string.Empty;
        }

        var s = subDir.Trim().Replace('\\', '/');
        while (s.StartsWith('/'))
        {
            s = s[1..];
        }

        if (s.Contains("..", StringComparison.Ordinal))
        {
            throw new UserFriendlyException("subDir 不能包含 ..");
        }

        return s;
    }

    private static string BuildPictureUrl(string? subDir, string fileName)
    {
        var s = string.IsNullOrWhiteSpace(subDir) ? string.Empty : $"/{subDir.Trim().Replace('\\', '/').Trim('/')}";
        return $"/picture{s}/{fileName}";
    }
}