using OrderSupportTicket.Models; using System; using System.Collections.Generic; using System.Linq; using System.Windows.Threading; namespace OrderSupportTicket.Services; /// /// 应用级工单实时监视器。SSE 连接的生命周期与应用一致,不随工单页关闭而停止。 /// public sealed class TicketRealtimeMonitor : IDisposable { private readonly object _sync = new object(); private readonly HashSet _seenEventIds = new HashSet(StringComparer.Ordinal); private readonly Dictionary _unreadMessages = new Dictionary(StringComparer.Ordinal); private readonly Dispatcher _dispatcher; private TicketClientOptions _options; private string _optionsFingerprint; private TicketRealtimeNotification _latestNotification; private bool _subscribed; private bool _disposed; private TicketRealtimeMonitor() { RealtimeService = new EventSourceTicketRealtimeService(); RealtimeService.NotificationReceived += OnNotificationReceived; _subscribed = true; _dispatcher = System.Windows.Application.Current?.Dispatcher ?? Dispatcher.CurrentDispatcher; } /// /// 全局唯一的工单实时监视器。 /// public static TicketRealtimeMonitor Current { get; } = new TicketRealtimeMonitor(); /// /// 供工单页复用的实时服务,避免重复建立 SSE 连接。 /// public EventSourceTicketRealtimeService RealtimeService { get; } /// /// 收到新的工单留言通知时触发。 /// public event EventHandler NewMessageReceived; /// /// 未读工单数量变化时触发。 /// public event EventHandler UnreadCountChanged; /// /// 当前未读工单数量。 /// public int UnreadCount { get { lock (_sync) return _unreadMessages.Values.Sum(); } } /// /// 是否已经启动过实时连接。 /// public bool IsStarted { get { lock (_sync) return _options != null; } } /// /// 最近一条新消息通知,供主页面点击角标时定位工单。 /// public TicketRealtimeNotification LatestNotification { get { lock (_sync) return _latestNotification; } } /// /// 启动或复用实时连接。相同账号和 APPID 重复调用不会重建连接。 /// public async Task Start(TicketClientOptions options) { if (_disposed) throw new ObjectDisposedException(nameof(TicketRealtimeMonitor)); if (options == null) throw new ArgumentNullException(nameof(options)); TicketApiService.ValidateOptions(options); var fingerprint = BuildFingerprint(options); lock (_sync) { if (string.Equals(_optionsFingerprint, fingerprint, StringComparison.Ordinal) && RealtimeService.State != TicketRealtimeState.Disconnected) { return; } if (!string.Equals(_optionsFingerprint, fingerprint, StringComparison.Ordinal)) { _seenEventIds.Clear(); } _options = options; _optionsFingerprint = fingerprint; } await RealtimeService.Start(options); } /// /// 将指定工单标记为已读。 /// public void MarkRead(string appId, string wechatOrderId) { if (string.IsNullOrWhiteSpace(appId) || string.IsNullOrWhiteSpace(wechatOrderId)) return; var key = BuildTicketKey(appId, wechatOrderId); var changed = false; lock (_sync) changed = _unreadMessages.Remove(key); if (changed) RaiseUnreadCountChanged(); } /// /// 合并历史未读数据。与实时事件产生的未读取较大值,避免重复计数。 /// /// /// 键为 "appId|wechatOrderId",值为该工单的未读消息数。 /// public void MergeUnread(IDictionary unreadByTicket) { if (unreadByTicket == null || unreadByTicket.Count == 0) return; var changed = false; lock (_sync) { foreach (var item in unreadByTicket) { if (string.IsNullOrWhiteSpace(item.Key) || item.Value <= 0) continue; _unreadMessages.TryGetValue(item.Key, out var current); if (item.Value <= current) continue; _unreadMessages[item.Key] = item.Value; changed = true; } } if (changed) RaiseUnreadCountChanged(); } /// /// 清空全部未读提示。 /// public void MarkAllRead() { var changed = false; lock (_sync) { if (_unreadMessages.Count > 0) { _unreadMessages.Clear(); changed = true; } } if (changed) RaiseUnreadCountChanged(); } public void Dispose() { if (_disposed) return; _disposed = true; if (_subscribed) { RealtimeService.NotificationReceived -= OnNotificationReceived; _subscribed = false; } RealtimeService.Dispose(); } private void OnNotificationReceived( object sender, TicketRealtimeNotificationEventArgs args) { var notification = args?.Notification; if (notification == null || notification.IsResyncRequired) return; if (string.IsNullOrWhiteSpace(notification.AppId) || string.IsNullOrWhiteSpace(notification.WechatOrderId)) { return; } if (!IsMessageAdded(notification)) return; if (!string.IsNullOrWhiteSpace(notification.EventId)) { lock (_sync) { if (!_seenEventIds.Add(notification.EventId)) return; if (_seenEventIds.Count > 5000) _seenEventIds.Clear(); } } var key = BuildTicketKey(notification.AppId, notification.WechatOrderId); int unreadCount; lock (_sync) { _unreadMessages.TryGetValue(key, out var ticketUnreadCount); _unreadMessages[key] = ticketUnreadCount + 1; _latestNotification = notification; unreadCount = _unreadMessages.Values.Sum(); } _dispatcher.BeginInvoke(new Action(() => { if (_disposed) return; NewMessageReceived?.Invoke( this, new TicketInboxMessageEventArgs(notification, unreadCount)); UnreadCountChanged?.Invoke( this, new TicketUnreadCountChangedEventArgs(unreadCount)); })); } private void RaiseUnreadCountChanged() { var unreadCount = UnreadCount; _dispatcher.BeginInvoke(new Action(() => { if (!_disposed) UnreadCountChanged?.Invoke( this, new TicketUnreadCountChangedEventArgs(unreadCount)); })); } private static bool IsMessageAdded(TicketRealtimeNotification notification) { if (!string.IsNullOrWhiteSpace(notification.MessageId)) return true; return !string.IsNullOrWhiteSpace(notification.Type) && notification.Type.IndexOf( "message_added", StringComparison.OrdinalIgnoreCase) >= 0; } private static string BuildTicketKey(string appId, string wechatOrderId) { return appId + "|" + wechatOrderId; } private static string BuildFingerprint(TicketClientOptions options) { var appIds = (options.AppIds ?? Enumerable.Empty()) .Where(appId => !string.IsNullOrWhiteSpace(appId)) .Select(appId => appId.Trim()) .Distinct(StringComparer.Ordinal) .OrderBy(appId => appId, StringComparer.Ordinal); return options.UserName + "|" + string.Join(",", appIds); } } /// /// 主页面收到的工单新消息事件参数。 /// public sealed class TicketInboxMessageEventArgs : EventArgs { public TicketInboxMessageEventArgs( TicketRealtimeNotification notification, int unreadCount) { Notification = notification; UnreadCount = unreadCount; } /// /// 服务端推送的原始通知。 /// public TicketRealtimeNotification Notification { get; } /// /// 收到该通知后的未读工单数量。 /// public int UnreadCount { get; } } /// /// 未读工单数量变化事件参数。 /// public sealed class TicketUnreadCountChangedEventArgs : EventArgs { public TicketUnreadCountChangedEventArgs(int unreadCount) { UnreadCount = unreadCount; } /// /// 当前未读工单数量。 /// public int UnreadCount { get; } }