This commit is contained in:
2026-03-30 11:07:30 +08:00
parent 2c44b3a4b2
commit d4a8e71733
74 changed files with 1751 additions and 421 deletions

View File

@@ -0,0 +1,64 @@
namespace StopShopping.FileApi.Services;
/// <summary>
/// 强类型返回值
/// </summary>
/// <typeparam name="T"></typeparam>
public class ApiResponse<T>
{
public ApiResponse()
{ }
public ApiResponse(T data)
{
Data = data;
}
/// <summary>
/// 是否成功
/// </summary>
/// <value></value>
public bool IsSucced { get; set; } = true;
/// <summary>
/// 错误消息
/// </summary>
/// <value></value>
public string? Message { get; set; }
/// <summary>
/// 关联数据
/// </summary>
/// <value></value>
public T? Data { get; set; }
public ApiResponse<T> Failed(string message)
{
IsSucced = false;
Message = message;
return this;
}
}
/// <summary>
/// 强类型返回值,只返回成功与否和消息
/// </summary>
public class ApiResponse : ApiResponse<object?>
{
public ApiResponse(bool isSucced = true, string message = "")
{
IsSucced = isSucced;
Message = message;
}
public static ApiResponse Succed(string message = "")
{
return new ApiResponse(message: message);
}
public static new ApiResponse Failed(string message)
{
return new ApiResponse(false, message);
}
}

View File

@@ -0,0 +1,6 @@
namespace StopShopping.FileApi.Services;
public class AppOptions
{
public string Domain { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,34 @@
using FileSignatures;
using FileSignatures.Formats;
using StopShopping.FileApi.Services.Implementions;
namespace StopShopping.FileApi.Services;
public static class Extensions
{
public static IServiceCollection AddServices(this IServiceCollection services, IConfiguration appOptions)
{
services.Configure<AppOptions>(appOptions);
services.AddValidation();
var imageFormats = FileFormatLocator.GetFormats().OfType<Image>();
var imageInspector = new FileFormatInspector(imageFormats);
services.AddSingleton<IFileFormatInspector>(imageInspector);
services.AddScoped<IFileService, FileService>();
return services;
}
public static string GetTargetDirectory(this UploadScences uploadScences)
{
return uploadScences switch
{
UploadScences.Avatar => "avatar",
UploadScences.Product => "product",
UploadScences.Category => "category",
_ => throw new ArgumentOutOfRangeException(nameof(uploadScences))
};
}
}

View File

@@ -0,0 +1,18 @@
namespace StopShopping.FileApi.Services;
/// <summary>
/// 文件上传结果
/// </summary>
public class FileUploadResp
{
/// <summary>
/// 新名,上传后重命名
/// </summary>
/// <value></value>
public string NewName { get; set; } = string.Empty;
/// <summary>
/// Url
/// </summary>
/// <value></value>
public string Url { get; set; } = string.Empty;
}

View File

@@ -0,0 +1,14 @@
namespace StopShopping.FileApi.Services;
/// <summary>
/// 文件服务
/// </summary>
public interface IFileService
{
/// <summary>
/// 上传文件
/// </summary>
/// <param name="payload"></param>
/// <returns></returns>
Task<ApiResponse<FileUploadResp>> UploadFileAsync(UploadParams payload);
}

View File

@@ -0,0 +1,59 @@
using Microsoft.Extensions.Options;
namespace StopShopping.FileApi.Services.Implementions;
public class FileService : IFileService
{
public FileService(IOptions<AppOptions> appOptions,
IWebHostEnvironment webHostEnvironment)
{
_appOptions = appOptions.Value;
_env = webHostEnvironment;
}
private readonly IWebHostEnvironment _env;
private readonly AppOptions _appOptions;
public async Task<ApiResponse<FileUploadResp>> UploadFileAsync(UploadParams payload)
{
var newName = Guid.NewGuid().ToString("N").ToLower();
var extension = Path.GetExtension(payload.File!.FileName);
var newFullName = $"{newName}{extension}";
var relativeToRootPath = GetRelativeToRootPath(payload.Scences, newFullName);
var targetPath = Path.Combine(_env.WebRootPath, GetRelativeToRootPath(payload.Scences));
if (!Directory.Exists(targetPath))
{
Directory.CreateDirectory(targetPath);
}
using var file = new FileStream(
Path.Combine(_env.WebRootPath, relativeToRootPath),
FileMode.CreateNew,
FileAccess.Write);
await payload.File.CopyToAsync(file);
FileUploadResp result = new()
{
NewName = newFullName,
Url = GetFileUrl(payload.Scences, newFullName),
};
return new ApiResponse<FileUploadResp>(result);
}
private string GetFileUrl(UploadScences scences, string fileName)
{
return $"{_appOptions.Domain}/{GetRelativeToRootPath(scences, fileName)
.Replace(Path.DirectorySeparatorChar, '/')}";
}
private string GetRelativeToRootPath(UploadScences scences, string fileName = "")
{
return Path.Combine(
"images",
scences.GetTargetDirectory(),
fileName);
}
}

View File

@@ -0,0 +1,22 @@
using System.ComponentModel.DataAnnotations;
namespace StopShopping.FileApi.Services;
/// <summary>
/// url查询参数
/// </summary>
/// <value></value>
public record NameUrlParams
{
/// <summary>
/// 场景
/// </summary>
/// <value></value>
public UploadScences Scences { get; set; }
/// <summary>
/// 文件名
/// </summary>
/// <value></value>
[Required]
public string[] Names { get; set; } = [];
}

View File

@@ -0,0 +1,14 @@
namespace StopShopping.FileApi.Services;
/// <summary>
/// url查询响应
/// </summary>
/// <value></value>
public class NameUrlResp
{
/// <summary>
/// 文件名:文件链接
/// </summary>
/// <value></value>
public KeyValuePair<string, string>[] NameUrls { get; set; } = [];
}

View File

@@ -0,0 +1,23 @@
using System.ComponentModel.DataAnnotations;
namespace StopShopping.FileApi.Services;
/// <summary>
/// 上传
/// </summary>
/// <value></value>
public record UploadParams
{
/// <summary>
/// 场景
/// </summary>
/// <value></value>
public UploadScences Scences { get; set; }
/// <summary>
/// 文件
/// </summary>
/// <value></value>
[Required]
[ImageFileValidation(2 * 1024 * 1024)]
public IFormFile? File { get; set; }
}

View File

@@ -0,0 +1,20 @@
namespace StopShopping.FileApi.Services;
/// <summary>
/// 文件上传场景
/// </summary>
public enum UploadScences
{
/// <summary>
/// 头像
/// </summary>
Avatar,
/// <summary>
/// 商品
/// </summary>
Product,
/// <summary>
/// 商品分类
/// </summary>
Category,
}

View File

@@ -0,0 +1,49 @@
using FileSignatures;
namespace System.ComponentModel.DataAnnotations;
public class ImageFileValidationAttribute : ValidationAttribute
{
public ImageFileValidationAttribute(long length)
{
Length = length;
}
public long Length { get; set; }
protected override ValidationResult? IsValid(object? value, ValidationContext validationContext)
{
var fileInspector = validationContext.GetRequiredService<IFileFormatInspector>();
if (null == fileInspector)
{
return new ValidationResult("未配置文件验证器");
}
if (value is IFormFile file)
{
var result = Validate(fileInspector, file);
if (null != result)
return result;
}
else if (value is IFormFileCollection files)
{
foreach (var f in files)
{
var result = Validate(fileInspector, f);
if (null != result)
return result;
}
}
return ValidationResult.Success;
}
private ValidationResult? Validate(IFileFormatInspector fileInspector, IFormFile file)
{
if (file.Length > Length)
return new ValidationResult("文件太大");
var format = fileInspector.DetermineFileFormat(file.OpenReadStream());
if (null == format)
return new ValidationResult("文件格式不支持");
return null;
}
}