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
相关产品推荐
相关产品推荐

