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

使用Ajax与IProgress获取进度时状态值始终为0的问题排查

问题分析与解决方案

核心问题:控制器实例的请求作用域特性

ASP.NET Core中,控制器是请求作用域的——每一个HTTP请求都会创建一个全新的控制器实例。在GeneratePrintRecipeCards(POST请求)中更新的_generateRecipeCardsStatus,属于当前POST请求对应的控制器实例;而GeneratePrintRecipeCardsStatus(GET请求)每次被调用时,都会创建新的控制器实例,这个新实例的_generateRecipeCardsStatus是初始值0,自然读不到之前更新的进度。

此外还有两个次要问题:

  • DoAsync方法标记了async但内部无真正异步操作(ToList()、foreach均为同步执行),会阻塞POST请求线程,导致轮询的GET请求在任务结束前可能无法获取有效进度。
  • 服务中donePointer被重复自增(++donePointer和donePointer++),会导致进度计算错误。

解决方案

1. 使用全局存储共享进度值

放弃控制器私有字段,改用IMemoryCache全局存储进度值,确保不同请求能访问到同一进度数据:

public class MenuPrintoutController : BaseController
{
    private readonly IMenuLabelService _menuLabelService;
    private readonly IMemoryCache _cache;
    // 用用户标识+任务ID生成唯一缓存键,避免不同用户/任务进度冲突
    private string GetProgressCacheKey(int menuHeaderId) => $"RecipeCardsProgress_{User.Identity.Name}_{menuHeaderId}";

    public MenuPrintoutController(IMenuLabelService menuLabelService, IMemoryCache cache)
    {
        _menuLabelService = menuLabelService;
        _cache = cache;
    }

    public int GeneratePrintRecipeCardsStatus(int menuHeaderId)
    {
        return _cache.TryGetValue(GetProgressCacheKey(menuHeaderId), out int progress) ? progress : 0;
    }

    private void ReportProgress(int progress, int menuHeaderId)
    {
        // 设置10分钟过期时间,避免内存泄漏
        _cache.Set(GetProgressCacheKey(menuHeaderId), progress, TimeSpan.FromMinutes(10));
    }

    [HttpPost]
    [ValidateAntiForgeryToken]
    public async Task<IActionResult> GeneratePrintRecipeCards(int id, int menuHeaderId)
    {
        try
        {
            // 初始化进度为0
            _cache.Set(GetProgressCacheKey(menuHeaderId), 0, TimeSpan.FromMinutes(10));
            var progress = new Progress<int>(p => ReportProgress(p, menuHeaderId));
            
            var data = await _menuLabelService.DoAsync(menuHeaderId, progress);
            
            TempData.Put("PrintRecipeCards", data);
            // 任务完成后清除缓存
            _cache.Remove(GetProgressCacheKey(menuHeaderId));
            
            return Json(new { Success = true });
        }
        catch (Exception exp)
        {
            _cache.Remove(GetProgressCacheKey(menuHeaderId));
            return Json(new { ErrorMsg = exp.Message });
        }
    }
}

2. 修正服务中的异步逻辑与进度计算

将同步数据库操作改为异步,避免阻塞请求线程,同时修复donePointer重复自增问题:

public async Task<object> DoAsync(int menuHeaderId, IProgress<int> progress) 
{
    // 改用ToListAsync异步查询数据库
    var menuFoodItemsList = await _context.FoodItemUnits
        .Where(x => x.MenuSectionFoodItemUnits.Any(y => y.Menusection.MenuheaderId == menuHeaderId))
        .Select(x => x.Fooditem)
        .ToListAsync();
        
    var menuFoodItems = new List<PrintLabel>();
    var donePointer = 0;
    
    foreach (var menuFoodItem in menuFoodItemsList)
    {
        menuFoodItems.Add(menuFoodItem);
        // ... 执行业务逻辑
        
        donePointer++;
        // 计算进度,强制转为浮点除法避免整数截断
        var currentProgress = (int)((double)donePointer / menuFoodItemsList.Count * 100);
        progress.Report(currentProgress);
        
        // 可选:添加短暂延迟,避免进度更新过于频繁
        await Task.Delay(10);
    }
    
    return menuFoodItems;
}

3. (进阶)用SignalR替代轮询

若场景允许,推荐使用SignalR实现实时进度推送,比定时轮询更高效,也无需维护缓存:

  • 注册SignalR服务并创建进度Hub
  • 前端连接Hub,实时接收进度推送
  • 后台任务通过Hub向前端主动推送进度

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.21 07:15:45