You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

基于.NET 4.5.2与AngularJS的后台大文件下载方案求助

解决后台生成大Excel文件后的客户端下载问题

你的思路方向是对的——用后台任务避免长时间阻塞HTTP请求,但核心问题是客户端和后台任务之间缺少状态同步机制,不知道文件什么时候生成完毕。下面结合你的技术栈(.NET 4.5.2 + AngularJS),给出几个可落地的解决方案:

方案1:基于轮询的状态同步(最易实现)

这个方案的核心是:服务器返回任务ID给客户端,客户端定期轮询任务状态,待文件生成完成后触发下载。

后台(C#)实现步骤

  1. 定义任务状态模型,用来追踪文件生成进度:
public class FileTaskStatus
{
    public string TaskId { get; set; }
    public string FilePath { get; set; }
    public TaskState State { get; set; }
    public string ErrorMsg { get; set; }
}

public enum TaskState
{
    InProgress,
    Completed,
    Failed
}
  1. 编写三个API接口:初始化任务、查询状态、下载文件
using System.Runtime.Caching;

// 初始化文件生成任务
[HttpPost]
public IHttpActionResult StartExcelGeneration()
{
    var taskId = Guid.NewGuid().ToString();
    var taskStatus = new FileTaskStatus
    {
        TaskId = taskId,
        State = TaskState.InProgress
    };
    // 用MemoryCache存储任务状态,设置2小时过期防止内存泄漏
    MemoryCache.Default.Add(taskId, taskStatus, DateTimeOffset.Now.AddHours(2));

    // 后台执行文件生成
    HostingEnvironment.QueueBackgroundWorkItem(async ct =>
    {
        try
        {
            var tempFilePath = Path.Combine(Path.GetTempPath(), $"{taskId}.xlsx");
            // 替换成你的Excel生成逻辑,记得传入ct支持任务取消
            await GenerateLargeExcel(tempFilePath, ct);

            taskStatus.State = TaskState.Completed;
            taskStatus.FilePath = tempFilePath;
        }
        catch (Exception ex)
        {
            taskStatus.State = TaskState.Failed;
            taskStatus.ErrorMsg = ex.Message;
        }
    });

    return Ok(new { TaskId = taskId });
}

// 查询任务状态
[HttpGet]
public IHttpActionResult CheckTaskStatus(string taskId)
{
    if (!MemoryCache.Default.Contains(taskId))
        return NotFound();
    
    var status = (FileTaskStatus)MemoryCache.Default[taskId];
    return Ok(new { status.State, status.ErrorMsg });
}

// 下载生成好的文件
[HttpGet]
public HttpResponseMessage DownloadExcel(string taskId)
{
    if (!MemoryCache.Default.Contains(taskId))
        return Request.CreateResponse(HttpStatusCode.NotFound);
    
    var status = (FileTaskStatus)MemoryCache.Default[taskId];
    if (status.State != TaskState.Completed)
        return Request.CreateResponse(HttpStatusCode.BadRequest, "文件尚未生成完成");
    
    var fileInfo = new FileInfo(status.FilePath);
    if (!fileInfo.Exists)
        return Request.CreateResponse(HttpStatusCode.NotFound);

    var stream = new FileStream(status.FilePath, FileMode.Open, FileAccess.Read);
    var response = Request.CreateResponse(HttpStatusCode.OK);
    response.Content = new StreamContent(stream);
    response.Content.Headers.ContentType = new MediaTypeHeaderValue("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
    response.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment")
    {
        FileName = "large-data.xlsx"
    };

    // 下载后删除临时文件(可选,也可以定时清理)
    HostingEnvironment.QueueBackgroundWorkItem(_ => File.Delete(status.FilePath));
    MemoryCache.Default.Remove(taskId);

    return response;
}

前端(AngularJS)实现

app.controller('ExcelDownloadCtrl', function($scope, $http, $interval) {
    $scope.startDownload = function() {
        $scope.status = "正在生成文件...";
        
        $http.post('/api/excel/start')
            .then(function(res) {
                var taskId = res.data.TaskId;
                // 每3秒轮询一次状态
                var pollTimer = $interval(function() {
                    $http.get('/api/excel/checkstatus', { params: { taskId: taskId } })
                        .then(function(statusRes) {
                            var state = statusRes.data.State;
                            if (state === "Completed") {
                                $interval.cancel(pollTimer);
                                $scope.status = "文件已就绪,开始下载...";
                                // 创建隐藏a标签触发下载
                                var downloadLink = document.createElement('a');
                                downloadLink.href = `/api/excel/download?taskId=${taskId}`;
                                downloadLink.download = "large-data.xlsx";
                                document.body.appendChild(downloadLink);
                                downloadLink.click();
                                document.body.removeChild(downloadLink);
                                $scope.status = "";
                            } else if (state === "Failed") {
                                $interval.cancel(pollTimer);
                                $scope.status = `生成失败:${statusRes.data.ErrorMsg}`;
                            }
                        });
                }, 3000);
            });
    };
});

方案2:用SignalR实现服务器主动通知(更实时)

如果不想用轮询,可以用SignalR让服务器在文件生成完成后主动推送消息给客户端,实时触发下载。

后台关键代码

  1. 先安装SignalR 2.x的NuGet包,然后创建Hub:
public class FileNotifyHub : Hub
{
    // 客户端注册任务ID,关联连接ID
    public void RegisterTask(string taskId)
    {
        MemoryCache.Default.Add($"TaskConn_{taskId}", Context.ConnectionId, DateTimeOffset.Now.AddHours(2));
    }
}
  1. 修改后台任务完成后的逻辑,添加推送:
HostingEnvironment.QueueBackgroundWorkItem(async ct =>
{
    try
    {
        // ... 生成Excel的逻辑 ...
        taskStatus.State = TaskState.Completed;
        taskStatus.FilePath = tempFilePath;

        // 推送完成消息给客户端
        var connId = (string)MemoryCache.Default[$"TaskConn_{taskId}"];
        if (!string.IsNullOrEmpty(connId))
        {
            var hubContext = GlobalHost.ConnectionManager.GetHubContext<FileNotifyHub>();
            await hubContext.Clients.Client(connId).OnFileReady(taskId);
        }
    }
    catch (Exception ex)
    {
        // ... 错误处理 ...
        var connId = (string)MemoryCache.Default[$"TaskConn_{taskId}"];
        if (!string.IsNullOrEmpty(connId))
        {
            var hubContext = GlobalHost.ConnectionManager.GetHubContext<FileNotifyHub>();
            await hubContext.Clients.Client(connId).OnFileFailed(ex.Message);
        }
    }
});

前端关键代码

app.controller('ExcelDownloadCtrl', function($scope, $http) {
    $scope.startDownload = function() {
        $scope.status = "正在生成文件...";
        
        // 连接SignalR Hub
        var hub = $.connection.fileNotifyHub;
        
        // 监听服务器推送的完成事件
        hub.client.OnFileReady = function(taskId) {
            $scope.$apply(function() {
                $scope.status = "文件已就绪,开始下载...";
                // 触发下载逻辑同方案1
                var downloadLink = document.createElement('a');
                downloadLink.href = `/api/excel/download?taskId=${taskId}`;
                downloadLink.download = "large-data.xlsx";
                document.body.appendChild(downloadLink);
                downloadLink.click();
                document.body.removeChild(downloadLink);
                $scope.status = "";
            });
        };
        
        hub.client.OnFileFailed = function(errorMsg) {
            $scope.$apply(function() {
                $scope.status = `生成失败:${errorMsg}`;
            });
        };
        
        // 启动连接后发起任务
        $.connection.hub.start().done(function() {
            $http.post('/api/excel/start')
                .then(function(res) {
                    var taskId = res.data.TaskId;
                    // 注册任务到Hub
                    hub.server.registerTask(taskId);
                });
        });
    };
});

方案3:更可靠的后台任务替代方案

HostingEnvironment.QueueBackgroundWorkItem的缺点是应用池回收时可能会中断任务,如果需要更高的可靠性,可以用Hangfire(支持.NET 4.5.2),它会将任务持久化到数据库,即使应用重启也能继续执行。

只需替换任务启动的逻辑:

// 安装Hangfire的NuGet包后,在Startup配置
public void Configuration(IAppBuilder app)
{
    GlobalConfiguration.Configuration.UseSqlServerStorage("你的数据库连接字符串");
    app.UseHangfireDashboard();
    app.UseHangfireServer();
}

// 初始化任务的API
[HttpPost]
public IHttpActionResult StartExcelGeneration()
{
    var taskId = Guid.NewGuid().ToString();
    var taskStatus = new FileTaskStatus
    {
        TaskId = taskId,
        State = TaskState.InProgress
    };
    MemoryCache.Default.Add(taskId, taskStatus, DateTimeOffset.Now.AddHours(2));

    // 用Hangfire替代QueueBackgroundWorkItem
    BackgroundJob.Enqueue(() => GenerateExcelAndUpdateStatus(taskId));

    return Ok(new { TaskId = taskId });
}

// 单独的任务方法
public void GenerateExcelAndUpdateStatus(string taskId)
{
    var taskStatus = (FileTaskStatus)MemoryCache.Default[taskId];
    try
    {
        var tempFilePath = Path.Combine(Path.GetTempPath(), $"{taskId}.xlsx");
        GenerateLargeExcel(tempFilePath, CancellationToken.None);
        taskStatus.State = TaskState.Completed;
        taskStatus.FilePath = tempFilePath;
    }
    catch (Exception ex)
    {
        taskStatus.State = TaskState.Failed;
        taskStatus.ErrorMsg = ex.Message;
    }
}

注意事项

  1. 临时文件清理:不管用哪种方案,都要定期清理服务器上的临时Excel文件(比如用定时任务删除24小时前的文件),避免磁盘空间耗尽。
  2. 多服务器场景:如果部署了多台服务器,MemoryCache无法跨服务器共享,需要改用分布式缓存(比如Redis)或数据库存储任务状态。
  3. 权限控制:如果需要限制用户只能下载自己生成的文件,要在任务状态中关联用户ID,下载时验证当前用户是否匹配。

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

火山引擎 最新活动