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

ASP.NET Core MVC中如何用SignalR实现Controller向前端请求确认并继续执行

基于SignalR实现Action方法等待前端确认的停车校验方案

核心思路

前端发起的AJAX请求是短连接,服务端Action默认执行完就会返回结果,无法直接挂起等待用户交互。我们通过「SignalR双向推送确认请求 + 全局请求状态存储 + 异步等待机制」实现需求,全程不中断原Action的执行链路。校验触发后向前端推送的提示文案为:

你确定要在此处停车吗?

实现步骤

步骤1:实现SignalR确认Hub

using Microsoft.AspNetCore.SignalR;
using System.Collections.Concurrent;

public class ParkConfirmHub : Hub
{
    // 单实例部署用内存存储即可,多实例部署请替换为Redis等分布式缓存
    public static ConcurrentDictionary<string, bool?> ConfirmResults = new();
    public static ConcurrentDictionary<string, SemaphoreSlim> WaitLocks = new();

    /// <summary>
    /// 前端收到确认提示后调用此方法回传用户选择结果
    /// </summary>
    public async Task SubmitConfirmResult(string requestId, bool isConfirm)
    {
        if (ConfirmResults.ContainsKey(requestId))
        {
            ConfirmResults[requestId] = isConfirm;
            if (WaitLocks.TryRemove(requestId, out var semaphore))
            {
                // 释放等待信号,唤醒Controller中挂起的线程
                semaphore.Release();
                await semaphore.DisposeAsync();
            }
        }
    }
}

步骤2:注册SignalR服务

在Program.cs中添加以下配置:

// 注册SignalR服务
builder.Services.AddSignalR();

// 中间件配置段添加Hub路由映射
app.MapHub<ParkConfirmHub>("/parkConfirmHub");

步骤3:改造ParkCar动作方法

using Microsoft.AspNetCore.SignalR;
using System.Security.Claims;

public class CarController : Controller
{
    private readonly ICarsService _carsService;
    private readonly IHubContext<ParkConfirmHub> _hubContext;

    public CarController(ICarsService carsService, IHubContext<ParkConfirmHub> hubContext)
    {
        _carsService = carsService;
        _hubContext = hubContext;
    }

    public async Task<IActionResult> ParkCar(string licenseplate, double lat, double lng)
    {
        var car = _carsService.GetCarByLicensePlate(licenseplate);
        if (car == null) return BadRequest("待停放车辆不存在");

        // 统计周边1公里内的车辆数量
        int nearbyCarCount = _carsService.GetNearbyCarCount(lat, lng, 1000);
        // 假设阈值为20辆,超出则触发用户确认逻辑
        if (nearbyCarCount > 20)
        {
            // 生成本次请求的唯一标识,建议拼接用户ID避免不同用户串单
            string requestId = Guid.NewGuid().ToString("N");
            try
            {
                // 初始化等待信号和结果存储
                var semaphore = new SemaphoreSlim(0, 1);
                ParkConfirmHub.WaitLocks.TryAdd(requestId, semaphore);
                ParkConfirmHub.ConfirmResults.TryAdd(requestId, null);

                // 向当前用户推送确认提示,用户ID根据自身鉴权逻辑获取即可
                string userId = User.FindFirstValue(ClaimTypes.NameIdentifier);
                await _hubContext.Clients.User(userId).SendAsync("ShowParkConfirm", new
                {
                    RequestId = requestId,
                    Message = "你确定要在此处停车吗?"
                });

                // 最多等待30秒,避免线程永久阻塞占用资源
                bool waitSuccess = await semaphore.WaitAsync(TimeSpan.FromSeconds(30));
                if (!waitSuccess)
                {
                    return Ok(new { success = false, msg = "确认超时,停放失败" });
                }

                // 读取用户确认结果
                ParkConfirmHub.ConfirmResults.TryRemove(requestId, out bool? isConfirm);
                if (isConfirm != true)
                {
                    return Ok(new { success = false, msg = "用户取消停放操作" });
                }
            }
            finally
            {
                // 清理状态避免内存泄漏
                ParkConfirmHub.ConfirmResults.TryRemove(requestId, out _);
                if (ParkConfirmHub.WaitLocks.TryRemove(requestId, out var sem))
                {
                    await sem.DisposeAsync();
                }
            }
        }

        // 无需确认或用户确认后执行停车逻辑
        _carsService.ParkCar(car, lat, lng);
        _carsService.FindAndAssignCityToCar(car);

        return Ok(new { success = true, msg = "车辆停放成功" });
    }
}

步骤4:前端逻辑实现

// 初始化SignalR连接
const connection = new signalR.HubConnectionBuilder()
    .withUrl("/parkConfirmHub")
    .build();

// 监听服务端推送的确认提示
connection.on("ShowParkConfirm", function (data) {
    const userConfirm = confirm(data.Message);
    // 回传用户选择结果
    connection.invoke("SubmitConfirmResult", data.RequestId, userConfirm);
});

// 启动连接
connection.start().catch(err => console.error("SignalR连接失败:", err));

// 原停车按钮AJAX请求逻辑无需修改
document.querySelector("#parkBtn").addEventListener("click", async function () {
    const licenseplate = document.querySelector("#selectedCar").value;
    const position = await getCurrentPosition(); // 替换为自身项目的定位获取方法
    const res = await fetch("/Car/ParkCar", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({
            licenseplate: licenseplate,
            lat: position.latitude,
            lng: position.longitude
        })
    }).then(r => r.json());
    alert(res.msg);
});

注意事项

  • 多实例部署场景下,不要使用内存的ConcurrentDictionary存储状态,请替换为Redis等分布式缓存,同时使用分布式锁代替SemaphoreSlim
  • 确认超时时间可根据业务需求调整,避免服务端线程被长时间占用
  • 推送消息时建议加上用户ID校验,避免不同用户的确认请求串单

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.03 05:06:03