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

ASP.NET Core API如何判断用户下载ZIP文件是否完成

结论先行

你当前的实现无法直接判断文件传输是否完成,因为当你在控制器中返回FileStreamResult后,控制器方法的执行上下文就已释放,文件流的写入、传输操作由ASP.NET Core底层管道接管,不会回调原控制器逻辑。但服务端完全可以实现下载完成状态的追踪,以下是两种常用的低侵入实现方案:


方案1:自定义可追踪的FileStreamResult(推荐)

重写FileStreamResult的执行逻辑,在流写入完成/出错时触发回调,你可以直接把下载统计的逻辑放到对应的回调中,不需要改动原有业务逻辑的结构:

public class TrackedFileStreamResult : FileStreamResult
{
    // 传输完成回调
    private readonly Action _onTransferCompleted;
    // 传输出错回调
    private readonly Action<Exception> _onTransferFailed;
    // 文件总字节数,用于判断是否完整传输
    private readonly long _totalFileLength;

    public TrackedFileStreamResult(Stream fileStream, string contentType, 
        Action onCompleted, Action<Exception> onFailed, long totalLength) 
        : base(fileStream, contentType)
    {
        _onTransferCompleted = onCompleted;
        _onTransferFailed = onFailed;
        _totalFileLength = totalLength;
    }

    public override async Task ExecuteResultAsync(ActionContext context)
    {
        long writtenBytes = 0;
        try
        {
            // 自己控制流写入,统计实际传输的字节数
            var buffer = new byte[1024 * 64];
            await using var outputStream = context.HttpContext.Response.Body;
            while (await FileStream.ReadAsync(buffer) is var readBytes && readBytes > 0)
            {
                await outputStream.WriteAsync(buffer.AsMemory(0, readBytes));
                writtenBytes += readBytes;
            }

            // 写入字节数等于文件总长度,判定为完整传输
            if (writtenBytes == _totalFileLength)
            {
                _onTransferCompleted?.Invoke();
            }
        }
        catch (Exception ex)
        {
            // 捕获到的异常通常为客户端断开连接、网络中断等传输异常
            _onTransferFailed?.Invoke(ex);
            // 可根据需要决定是否向外抛出异常
        }
        finally
        {
            await FileStream.DisposeAsync();
        }
    }
}

你原来的控制器返回逻辑修改为:

if (System.IO.File.Exists(artPackFilePath))
{
    var fileInfo = new FileInfo(artPackFilePath);
    var fileStream = System.IO.File.OpenRead(artPackFilePath);
    return new TrackedFileStreamResult(
        fileStream, 
        "application/zip",
        // 传输完成后再更新数据库统计
        () => {
            // 你的下载次数+1、记录用户信息的逻辑
        },
        // 传输出错的逻辑,可记录部分下载的日志
        (ex) => {
            _logger.LogWarning(ex, "文件部分下载,用户{UserId},文件{FilePath}", requestDto.UserId, artPackFilePath);
        },
        fileInfo.Length
    );
}

方案2:通过响应完成回调中间件实现

如果不想修改控制器返回逻辑,可以加一个全局中间件监听下载接口的响应完成事件:

app.Use(async (context, next) =>
{
    // 匹配你的下载接口路由
    if (context.Request.Path.Equals("/api/xxx/DownloadDesign", StringComparison.OrdinalIgnoreCase))
    {
        // 提前从RequestDto中提取需要的用户、文件标识,存到HttpContext.Items
        // 注意:如果是Post请求,要允许请求体重复读取,避免后续模型绑定失败
        context.Request.EnableBuffering();
        // 读取RequestDto的逻辑此处省略,读取后记得把请求体位置重置为0
        context.Response.OnCompleted(async () =>
        {
            if (context.Response.StatusCode == StatusCodes.Status200OK)
            {
                // 此处写下载完成的统计逻辑
                // 注意:OnCompleted中无法直接使用控制器的作用域服务,需要手动创建服务作用域
                using var scope = context.RequestServices.CreateScope();
                var dbContext = scope.ServiceProvider.GetRequiredService<你的数据库上下文类>();
                // 你的更新逻辑
            }
            await Task.CompletedTask;
        });
    }
    await next();
});

注意事项

  • 服务端无法100%确认客户端已将文件完整写入本地磁盘,以上方案判定的完成仅代表服务端已经将全部文件字节发送至网络栈,且未捕获到传输中断异常,这已经是服务端能获取到的最准确的传输状态。
  • 不要在控制器返回文件前就更新下载统计,避免用户点击下载后立即取消也被统计为有效下载。
  • 如果需要统计部分下载的进度,可以在流写入的回调中累加已写入的字节数,和文件总大小对比即可得到传输比例。

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.07 05:48:03