You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

EF Core与CQRS架构下在CreateHandler中实现FTP文件上传的咨询

CQRS架构下EF Core项目整合FTP文件上传到SliderCreateHandler的实现方案

1. 先改造现有FTP上传帮助类

原有帮助类存在3个问题:

  • UploadFtp为私有方法,外部无法调用
  • FTP账号、密码、地址硬编码在代码中,不可配置
  • 同步方法会阻塞请求线程,影响接口性能

改造后的代码如下:

namespace Medoper.Application.Helpers
{
    // 新增接口方便依赖注入
    public interface IFileUpload
    {
        // 支持传入文件流直接上传,无需存本地临时文件
        Task<string> UploadFtpAsync(string fileName, Stream fileStream);
        // 兼容原有本地文件路径上传的逻辑
        Task<string> UploadFtpAsync(string fileName, string filePath);
    }

    public class FileUpload : IFileUpload
    {
        // 从配置读取FTP参数,不要硬编码
        private readonly FtpConfig _ftpConfig;
        public FileUpload(IOptions<FtpConfig> ftpConfig)
        {
            _ftpConfig = ftpConfig.Value;
        }

        public async Task<string> UploadFtpAsync(string fileName, Stream fileStream)
        {
            var ftpFullPath = $"{_ftpConfig.CdnAddress}/asd/{fileName}";
            FtpWebRequest request = (FtpWebRequest)WebRequest.Create(ftpFullPath);
            request.Method = WebRequestMethods.Ftp.UploadFile;
            request.Credentials = new NetworkCredential(_ftpConfig.CdnName, _ftpConfig.CdnPassword);

            using (Stream requestStream = request.GetRequestStream())
            {
                await fileStream.CopyToAsync(requestStream);
            }

            using (FtpWebResponse response = (FtpWebResponse)await request.GetResponseAsync())
            {
                // 返回可访问的文件CDN地址,用于存到数据库
                return $"{_ftpConfig.CdnPublicDomain}/asd/{fileName}";
            }
        }

        public async Task<string> UploadFtpAsync(string fileName, string filePath)
        {
            using var fileStream = new FileStream(filePath, FileMode.Open, FileAccess.Read);
            return await UploadFtpAsync(fileName, fileStream);
        }
    }

    // FTP配置类,对应appsettings.json中的节点
    public class FtpConfig
    {
        public string CdnName { get; set; }
        public string CdnPassword { get; set; }
        public string CdnAddress { get; set; }
        // CDN公网访问域名,用户打开浏览器访问文件用的地址
        public string CdnPublicDomain { get; set; }
    }
}

然后在appsettings.json中新增FTP配置节点:

"FtpConfig": {
  "CdnName": "你的FTP账号",
  "CdnPassword": "你的FTP密码",
  "CdnAddress": "ftp://你的FTP服务器地址",
  "CdnPublicDomain": "https://你的CDN访问域名"
}

2. 注册服务到DI容器

在Program.cs中添加如下代码:

// 绑定FTP配置
builder.Services.Configure<FtpConfig>(builder.Configuration.GetSection("FtpConfig"));
// 注册文件上传服务
builder.Services.AddScoped<IFileUpload, FileUpload>();

3. 修改SliderCreate请求类

新增文件接收属性,让Swagger支持上传文件:

public class SliderCreate : IRequest<ApiResponse<SliderResponse>>
{
    // 原有其他字段不变
    public IFormFile SliderImage { get; set; }
}

4. 改造SliderCreateHandler整合上传逻辑

修改后的Handler代码如下:

public class SliderCreateHandler : IRequestHandler<SliderCreate, ApiResponse<SliderResponse>>
{
    private readonly IUnitOfWork _repo;
    private readonly IMapper _mapper;
    // 注入文件上传服务
    private readonly IFileUpload _fileUpload;

    public SliderCreateHandler(IUnitOfWork repo, IMapper mapper, IFileUpload fileUpload)
    {
        _repo = repo;
        _mapper = mapper;
        _fileUpload = fileUpload;
    }

    public async Task<ApiResponse<SliderResponse>> Handle(SliderCreate request, CancellationToken cancellationToken)
    {
        // 1. 先校验文件是否存在
        if (request.SliderImage == null || request.SliderImage.Length == 0)
        {
            return new ErrorApiResponse<SliderResponse>("请上传轮播图文件");
        }

        // 2. 生成唯一文件名,避免重名覆盖
        var fileExt = Path.GetExtension(request.SliderImage.FileName);
        var uniqueFileName = $"{Guid.NewGuid():N}{fileExt}";

        string filePublicUrl;
        try
        {
            // 3. 直接读取文件流上传FTP,无需存本地临时文件
            using var fileStream = request.SliderImage.OpenReadStream();
            filePublicUrl = await _fileUpload.UploadFtpAsync(uniqueFileName, fileStream);
        }
        catch (Exception ex)
        {
            // 上传失败直接返回错误,不执行后续数据库操作
            return new ErrorApiResponse<SliderResponse>($"文件上传失败:{ex.Message}");
        }

        // 4. 映射实体,把上传后的文件地址赋值到实体字段
        var mapped = _mapper.Map<Slider>(request);
        // 假设Slider实体有ImageUrl字段存文件地址
        mapped.ImageUrl = filePublicUrl;

        if (mapped == null)
            return new ErrorApiResponse<SliderResponse>(ResultMessage.NotCreatedBlog);

        // 5. 保存到数据库
        var model = await _repo.Sliders.AddAsync(mapped);
        // 这里可以加SaveChangesAsync的异常捕获,捕获到的话可以调用FTP删除逻辑回滚上传的文件
        await _repo.SaveChangesAsync(cancellationToken);

        var response = _mapper.Map<SliderResponse>(model);
        return new SuccessApiResponse<SliderResponse>(response);
    }
}

注意事项

  • 如果要求数据一致性更高,可以在SaveChangesAsync抛出异常时,新增FTP删除方法删除已上传的文件,避免产生垃圾文件
  • 可以新增文件格式、大小校验逻辑,避免上传非法文件
  • 生产环境建议给FTP请求加超时时间,避免请求长时间挂起

内容的提问来源于stack exchange,提问作者Burcu

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.09.23 20:24:01