.NET Bot Framework SDK v4:同时间提醒重复发送自适应卡片的解决方法
Teams机器人重复发送提醒卡片问题排查与解决
问题现象
使用Bot Framework SDK v4在Teams频道实现提醒功能,提醒数据存储于SharePoint,通过定时器在预设时间拉取提醒并发送自适应卡片。当提醒设置为不同时间时,每个提醒对应一张卡片,功能正常;但创建n个同一时间的提醒时,会收到n*n张重复卡片,不符合预期。曾尝试用字典以ReminderId为键去重,但未生效。
相关代码
private async Task SetReminderTimer(ITurnContext turnContext, ReminderListDto reminderTask, DateTime dateTime, string userInputTimeZoneOffset) { try { // Calculate the time until the reminder should trigger var delay = dateTime - DateTime.Now; _reminderTimer = new Timer(async _ => { // Retrieve the reminder item from SharePoint var reminders = await _sharePointLogger.GetReminderFromSharePointAsync(reminderTask, userInputTimeZoneOffset); foreach (var reminder in reminders) { // If the reminder item exists and the reminder time is in the past, send the reminder message to the user if (reminder != null && DateTime.Now > reminderTask.Time) { // Send a reminder message to the user var conversationReference = turnContext.Activity.GetConversationReference(); await ((BotAdapter)turnContext.Adapter).ContinueConversationAsync(_botId, conversationReference, async (turnContext, cancellationToken) => { var displayReminder = ReminderList.DisplayReminder(reminder.Time.ToString("HH:mm"), reminder.ReminderType, reminder.Priority, reminder.ReminderDetails); var adaptiveCardAttachment = new Attachment() { ContentType = "application/vnd.microsoft.card.adaptive", Content = JsonConvert.DeserializeObject(displayReminder), }; await turnContext.SendActivityAsync(MessageFactory.Attachment(adaptiveCardAttachment), cancellationToken); }, default); //await DeleteReminderFromSharePointAsync(reminder.ReminderId); } } }, null, (int)delay.TotalMilliseconds, Timeout.Infinite); } catch (Exception ex) { _logger.LogInformation($"error at sedning reminder to user {ex.Message} "); throw; } }
问题根源
- 定时器重复创建:每创建一个同时间的提醒,就会调用一次
SetReminderTimer,生成独立的定时器实例。当到达预设时间时,每个定时器都会执行回调逻辑,从SharePoint拉取所有同时间的提醒并逐个发送。例如创建3个同时间提醒,会生成3个定时器,每个定时器遍历3条提醒发送,最终产生9张重复卡片。 - 去重逻辑无效:之前尝试的字典去重仅在单个定时器的回调内部生效,每个定时器的字典是独立实例,无法跨定时器去重;或者字典未持久化,每次回调执行时重新初始化,起不到全局去重作用。
解决方案
方案一:按时间维度聚合提醒,统一管理定时器
维护全局字典,按触发时间(标准化到分钟级别,避免毫秒差异)分组管理提醒列表和定时器实例,确保同一时间仅存在一个定时器:
// 全局字典,线程安全操作需加锁 private readonly Dictionary<DateTime, (List<ReminderListDto> Reminders, Timer Timer)> _timedReminders = new Dictionary<DateTime, (List<ReminderListDto>, Timer)>(); private async Task SetReminderTimer(ITurnContext turnContext, ReminderListDto reminderTask, DateTime triggerTime, string userInputTimeZoneOffset) { try { // 标准化触发时间,消除毫秒级差异 var normalizedTime = new DateTime(triggerTime.Year, triggerTime.Month, triggerTime.Day, triggerTime.Hour, triggerTime.Minute, 0); lock (_timedReminders) { if (_timedReminders.TryGetValue(normalizedTime, out var existingEntry)) { // 已有对应定时器,直接添加提醒到列表 existingEntry.Reminders.Add(reminderTask); return; } var delay = normalizedTime - DateTime.Now; var newReminders = new List<ReminderListDto> { reminderTask }; var timer = new Timer(async _ => { // 遍历该时间下的所有提醒发送 foreach (var reminder in newReminders) { var conversationReference = turnContext.Activity.GetConversationReference(); await ((BotAdapter)turnContext.Adapter).ContinueConversationAsync(_botId, conversationReference, async (ctx, cancellationToken) => { var displayReminder = ReminderList.DisplayReminder(reminder.Time.ToString("HH:mm"), reminder.ReminderType, reminder.Priority, reminder.ReminderDetails); var adaptiveCardAttachment = new Attachment() { ContentType = "application/vnd.microsoft.card.adaptive", Content = JsonConvert.DeserializeObject(displayReminder), }; await ctx.SendActivityAsync(MessageFactory.Attachment(adaptiveCardAttachment), cancellationToken); }, default); // 发送后删除SharePoint中的提醒 await DeleteReminderFromSharePointAsync(reminder.ReminderId); } // 触发完成后移除全局字典中的对应条目 lock (_timedReminders) { _timedReminders.Remove(normalizedTime); } }, null, (int)delay.TotalMilliseconds, Timeout.Infinite); _timedReminders.Add(normalizedTime, (newReminders, timer)); } } catch (Exception ex) { _logger.LogInformation($"error at setting reminder timer: {ex.Message}"); throw; } }
方案二:通过SharePoint标记提醒发送状态
在SharePoint的提醒表中新增IsSent布尔字段(默认值false),发送前先标记状态,确保每个提醒仅发送一次:
// 修改定时器回调中的逻辑 var reminders = await _sharePointLogger.GetReminderFromSharePointAsync(reminderTask, userInputTimeZoneOffset); foreach (var reminder in reminders.Where(r => !r.IsSent && DateTime.Now > r.Time)) { // 先将提醒标记为已发送(需确保SharePoint操作的原子性,避免并发问题) await _sharePointLogger.MarkReminderAsSentAsync(reminder.ReminderId); // 再发送自适应卡片 var conversationReference = turnContext.Activity.GetConversationReference(); await ((BotAdapter)turnContext.Adapter).ContinueConversationAsync(_botId, conversationReference, async (turnContext, cancellationToken) => { var displayReminder = ReminderList.DisplayReminder(reminder.Time.ToString("HH:mm"), reminder.ReminderType, reminder.Priority, reminder.ReminderDetails); var adaptiveCardAttachment = new Attachment() { ContentType = "application/vnd.microsoft.card.adaptive", Content = JsonConvert.DeserializeObject(displayReminder), }; await turnContext.SendActivityAsync(MessageFactory.Attachment(adaptiveCardAttachment), cancellationToken); }, default); }
方案三:清理重复定时器(应急方案)
若暂时无法重构定时器管理逻辑,可在创建新定时器前,取消同一时间的旧定时器(不推荐长期使用,建议采用方案一或二):
- 维护全局定时器列表,创建新定时器时遍历列表,取消所有相同触发时间的旧定时器,再创建新的。
内容的提问来源于stack exchange,提问作者Abhay Gupta
相关产品推荐
相关产品推荐

