Files
29WxOrder/OrderSupportTicket/Services/TicketRealtimeMonitor.cs
T

292 lines
8.3 KiB
C#

using OrderSupportTicket.Models;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows.Threading;
namespace OrderSupportTicket.Services;
/// <summary>
/// 应用级工单实时监视器。SSE 连接的生命周期与应用一致,不随工单页关闭而停止。
/// </summary>
public sealed class TicketRealtimeMonitor : IDisposable
{
private readonly object _sync = new object();
private readonly HashSet<string> _seenEventIds =
new HashSet<string>(StringComparer.Ordinal);
private readonly Dictionary<string, int> _unreadMessages =
new Dictionary<string, int>(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;
}
/// <summary>
/// 全局唯一的工单实时监视器。
/// </summary>
public static TicketRealtimeMonitor Current { get; } = new TicketRealtimeMonitor();
/// <summary>
/// 供工单页复用的实时服务,避免重复建立 SSE 连接。
/// </summary>
public ITicketRealtimeService RealtimeService { get; }
/// <summary>
/// 收到新的工单留言通知时触发。
/// </summary>
public event EventHandler<TicketInboxMessageEventArgs> NewMessageReceived;
/// <summary>
/// 未读工单数量变化时触发。
/// </summary>
public event EventHandler<TicketUnreadCountChangedEventArgs> UnreadCountChanged;
/// <summary>
/// 当前未读工单数量。
/// </summary>
public int UnreadCount
{
get
{
lock (_sync)
return _unreadMessages.Values.Sum();
}
}
/// <summary>
/// 是否已经启动过实时连接。
/// </summary>
public bool IsStarted
{
get
{
lock (_sync)
return _options != null;
}
}
/// <summary>
/// 最近一条新消息通知,供主页面点击角标时定位工单。
/// </summary>
public TicketRealtimeNotification LatestNotification
{
get
{
lock (_sync)
return _latestNotification;
}
}
/// <summary>
/// 启动或复用实时连接。相同账号和 APPID 重复调用不会重建连接。
/// </summary>
public void 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;
}
_options = options;
_optionsFingerprint = fingerprint;
}
RealtimeService.Start(options);
}
/// <summary>
/// 将指定工单标记为已读。
/// </summary>
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();
}
/// <summary>
/// 清空全部未读提示。
/// </summary>
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<string>())
.Where(appId => !string.IsNullOrWhiteSpace(appId))
.Select(appId => appId.Trim())
.Distinct(StringComparer.Ordinal)
.OrderBy(appId => appId, StringComparer.Ordinal);
return options.UserName + "|" + string.Join(",", appIds);
}
}
/// <summary>
/// 主页面收到的工单新消息事件参数。
/// </summary>
public sealed class TicketInboxMessageEventArgs : EventArgs
{
public TicketInboxMessageEventArgs(
TicketRealtimeNotification notification,
int unreadCount)
{
Notification = notification;
UnreadCount = unreadCount;
}
/// <summary>
/// 服务端推送的原始通知。
/// </summary>
public TicketRealtimeNotification Notification { get; }
/// <summary>
/// 收到该通知后的未读工单数量。
/// </summary>
public int UnreadCount { get; }
}
/// <summary>
/// 未读工单数量变化事件参数。
/// </summary>
public sealed class TicketUnreadCountChangedEventArgs : EventArgs
{
public TicketUnreadCountChangedEventArgs(int unreadCount)
{
UnreadCount = unreadCount;
}
/// <summary>
/// 当前未读工单数量。
/// </summary>
public int UnreadCount { get; }
}