修改工单流程页面,订单列表增加工单按钮

This commit is contained in:
chfei committed 2026-10-18 20:35:47 +08:00
1 parent f9dcae4610
commit 10bb0803d9
44 files changed
+5920 -1291

No files matched your search

@@ -77,7 +77,7 @@ public sealed class EventSourceTicketRealtimeService : ITicketRealtimeService
{
try
{
await source.StartAsync().ConfigureAwait(false);
await source.StartAsync();
lock (_sync)
{
if (!_running || !ReferenceEquals(_eventSource, source))
@@ -0,0 +1,35 @@
using System.Threading;
using System.Threading.Tasks;
using System.Windows.Media;
namespace OrderSupportTicket.Services;
/// <summary>
/// 商品缩略图加载器,限制并发下载数量,避免工单列表同时发起大量请求。
/// </summary>
internal static class ProductImageLoader
{
private static readonly SemaphoreSlim Gate = new SemaphoreSlim(4, 4);
public static async Task<ImageSource> LoadAsync(string url)
{
if (string.IsNullOrWhiteSpace(url))
return null;
await Gate.WaitAsync().ConfigureAwait(false);
try
{
return await Task.Run(
() => AllBase.ImageBuild.LoadBitmipImageFromUrl(url.Trim()))
.ConfigureAwait(false);
}
catch
{
return null;
}
finally
{
Gate.Release();
}
}
}
+15 -10
View File
@@ -10,6 +10,7 @@ using System.Runtime.Serialization.Json;
using System.Text;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using WxOrder.Framework.Commons;
namespace OrderSupportTicket.Services;
@@ -18,7 +19,7 @@ namespace OrderSupportTicket.Services;
/// </summary>
public sealed class TicketApiService : ITicketApiService
{
internal const string ServerUrl = "https://ai.e57.cn";
internal static string ServerUrl = "https://ai.e57.cn";
private const string ApiRoot = "/api/open/v2/customer-work-orders";
private static readonly Regex AppIdRegex =
new Regex(@"^wx[0-9a-f]{16}$", RegexOptions.Compiled | RegexOptions.CultureInvariant);
@@ -28,7 +29,13 @@ public sealed class TicketApiService : ITicketApiService
new Regex(@"^[\p{L}\p{N}_.@-]{2,120}$", RegexOptions.Compiled);
private static readonly Regex OperationIdRegex =
new Regex(@"^[A-Za-z0-9_.:-]{1,128}$", RegexOptions.Compiled | RegexOptions.CultureInvariant);
static TicketApiService()
{
if (EnvironmentHelper.IsTest)
{
ServerUrl = "http://localhost:5188";
}
}
/// <summary>
/// 获取工单列表
/// </summary>
@@ -64,7 +71,7 @@ public sealed class TicketApiService : ITicketApiService
return await PostAsync<WorkOrderListResponse>(
ApiRoot + "/list",
request).ConfigureAwait(false);
request);
}
/// <summary>
/// 获取工单消息
@@ -100,8 +107,7 @@ public sealed class TicketApiService : ITicketApiService
var path = ApiRoot
+ "/messages";
return await PostAsync<WorkOrderMessagesResponse>(path, request)
.ConfigureAwait(false);
return await PostAsync<WorkOrderMessagesResponse>(path, request);
}
/// <summary>
/// 创建工单
@@ -170,8 +176,7 @@ public sealed class TicketApiService : ITicketApiService
+ "/orders/"
+ Uri.EscapeDataString(workOrder.WechatOrderId)
+ "/messages";
return await PostAsync<SendMessageResponse>(path, request)
.ConfigureAwait(false);
return await PostAsync<SendMessageResponse>(path, request);
}
public async Task MarkReadAsync(
@@ -196,7 +201,7 @@ public sealed class TicketApiService : ITicketApiService
+ "/orders/"
+ Uri.EscapeDataString(wechatOrderId)
+ "/read";
var html = await PostBodyAsync(path, request).ConfigureAwait(false);
var html = await PostBodyAsync(path, request);
EnsureSuccessBody(html);
}
@@ -245,7 +250,7 @@ public sealed class TicketApiService : ITicketApiService
string path,
object request)
{
var html = await PostBodyAsync(path, request).ConfigureAwait(false);
var html = await PostBodyAsync(path, request);
return DeserializeResponse<T>(html);
}
@@ -256,7 +261,7 @@ public sealed class TicketApiService : ITicketApiService
var url = BuildUri(path);
// 请求 DTO 使用 DataMember 驼峰字段,先序列化后走 GetHtml 的字符串重载。
var json = Serialize(request);
var html = await GetHtml.PostJsonAsync(url, json).ConfigureAwait(false);
var html = await GetHtml.PostJsonAsync(url, json);
if (string.IsNullOrWhiteSpace(html))
throw RequestFailed("工单接口未返回数据。");
return html;
@@ -0,0 +1,291 @@
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; }
}