增加工单模块

This commit is contained in:
chfei committed 2026-10-17 18:18:05 +08:00
1 parent dc136e449e
commit f9dcae4610
17 files changed
+4908

No files matched your search

@@ -0,0 +1,549 @@
using System.Collections.Generic;
using System.Runtime.Serialization;
namespace OrderSupportTicket.Models.Api;
/// <summary>
/// 创建工单请求。
/// </summary>
[DataContract]
internal sealed class CreateWorkOrderRequest
{
/// <summary>
/// 拍单账号。
/// </summary>
[DataMember(Name = "purchaseAccount")]
public string PurchaseAccount { get; set; }
/// <summary>
/// 订单所属店铺 APPID。
/// </summary>
[DataMember(Name = "appId")]
public string AppId { get; set; }
/// <summary>
/// 微信订单号,保持为数字字符串。
/// </summary>
[DataMember(Name = "wechatOrderId")]
public string WechatOrderId { get; set; }
/// <summary>
/// 首条问题描述,长度 1~2000 字。
/// </summary>
[DataMember(Name = "description")]
public string Description { get; set; }
/// <summary>
/// 可选的创建幂等编号。
/// </summary>
[DataMember(Name = "clientRequestId", EmitDefaultValue = false)]
public string ClientRequestId { get; set; }
}
/// <summary>
/// 工单摘要列表请求。
/// </summary>
[DataContract]
internal sealed class ListWorkOrdersRequest
{
/// <summary>
/// 拍单账号。
/// </summary>
[DataMember(Name = "purchaseAccount")]
public string PurchaseAccount { get; set; }
/// <summary>
/// 要查询的店铺 APPID 数组。
/// </summary>
[DataMember(Name = "appIds")]
public IList<string> AppIds { get; set; }
/// <summary>
/// 可选的微信订单号精确筛选条件。
/// </summary>
[DataMember(Name = "wechatOrderId", EmitDefaultValue = false)]
public string WechatOrderId { get; set; }
/// <summary>
/// 可选的工单状态筛选条件。
/// </summary>
[DataMember(Name = "status", EmitDefaultValue = false)]
public string Status { get; set; }
/// <summary>
/// 本页最大返回数量,范围 1~100。
/// </summary>
[DataMember(Name = "limit")]
public int Limit { get; set; }
/// <summary>
/// 下一页游标,使用服务端返回的原始值。
/// </summary>
[DataMember(Name = "cursor", EmitDefaultValue = false)]
public string Cursor { get; set; }
}
/// <summary>
/// 查询订单留言板请求。
/// </summary>
[DataContract]
internal sealed class GetMessagesRequest
{
/// <summary>
/// 拍单账号。
/// </summary>
[DataMember(Name = "purchaseAccount")]
public string PurchaseAccount { get; set; }
/// <summary>
/// 微信订单号,保持为数字字符串。
/// </summary>
[DataMember(Name = "wechatOrderId")]
public string WechatOrderId { get; set; }
/// <summary>
/// 订单所属店铺 APPID。
/// </summary>
[DataMember(Name = "appId")]
public string AppId { get; set; }
/// <summary>
/// 本页最大返回数量,范围 1~100。
/// </summary>
[DataMember(Name = "limit")]
public int Limit { get; set; }
/// <summary>
/// 加载更早留言时使用的 before 游标。
/// </summary>
[DataMember(Name = "before", EmitDefaultValue = false)]
public string Before { get; set; }
}
/// <summary>
/// 提交客户留言请求。
/// </summary>
[DataContract]
internal sealed class SendMessageRequest
{
/// <summary>
/// 拍单账号。
/// </summary>
[DataMember(Name = "purchaseAccount")]
public string PurchaseAccount { get; set; }
/// <summary>
/// 订单所属店铺 APPID。
/// </summary>
[DataMember(Name = "appId")]
public string AppId { get; set; }
/// <summary>
/// 留言幂等编号。
/// </summary>
[DataMember(Name = "clientMessageId")]
public string ClientMessageId { get; set; }
/// <summary>
/// 留言正文,长度 1~4000 字。
/// </summary>
[DataMember(Name = "text")]
public string Text { get; set; }
}
/// <summary>
/// 标记留言已读请求。
/// </summary>
[DataContract]
internal sealed class MarkReadRequest
{
/// <summary>
/// 拍单账号。
/// </summary>
[DataMember(Name = "purchaseAccount")]
public string PurchaseAccount { get; set; }
/// <summary>
/// 订单所属店铺 APPID。
/// </summary>
[DataMember(Name = "appId")]
public string AppId { get; set; }
/// <summary>
/// 已读到的最大留言序号。
/// </summary>
[DataMember(Name = "throughSequenceNo")]
public int ThroughSequenceNo { get; set; }
}
/// <summary>
/// 工单摘要分页响应。
/// </summary>
[DataContract]
public sealed class WorkOrderListResponse
{
/// <summary>
/// 请求是否成功。
/// </summary>
[DataMember(Name = "success")]
public bool Success { get; set; }
/// <summary>
/// 当前页工单摘要。
/// </summary>
[DataMember(Name = "items")]
public List<WorkOrderSummaryDto> Items { get; set; }
/// <summary>
/// 是否还有下一页。
/// </summary>
[DataMember(Name = "hasMore")]
public bool HasMore { get; set; }
/// <summary>
/// 下一页游标。
/// </summary>
[DataMember(Name = "nextCursor")]
public string NextCursor { get; set; }
/// <summary>
/// 接口返回的身份校验标记,当前恒为 false。
/// </summary>
[DataMember(Name = "identityVerified")]
public bool IdentityVerified { get; set; }
}
/// <summary>
/// 单个订单的工单摘要。
/// </summary>
[DataContract]
public sealed class WorkOrderSummaryDto
{
/// <summary>
/// 订单所属店铺 APPID。
/// </summary>
[DataMember(Name = "appId")]
public string AppId { get; set; }
/// <summary>
/// 微信订单号,保持为数字字符串。
/// </summary>
[DataMember(Name = "wechatOrderId")]
public string WechatOrderId { get; set; }
/// <summary>
/// 工单状态编码。
/// </summary>
[DataMember(Name = "status")]
public string Status { get; set; }
/// <summary>
/// 工单状态中文文本。
/// </summary>
[DataMember(Name = "statusText")]
public string StatusText { get; set; }
/// <summary>
/// 工单创建时间,由接口以 ISO 8601 字符串传输。
/// </summary>
[DataMember(Name = "createdAt")]
public string CreatedAt { get; set; }
/// <summary>
/// 工单更新时间,由接口以 ISO 8601 字符串传输。
/// </summary>
[DataMember(Name = "updatedAt")]
public string UpdatedAt { get; set; }
/// <summary>
/// 最近一条公开留言时间,由接口以 ISO 8601 字符串传输。
/// </summary>
[DataMember(Name = "lastMessageAt")]
public string LastMessageAt { get; set; }
/// <summary>
/// 最近一次客服正式回复时间;从未回复时为 null。
/// </summary>
[DataMember(Name = "lastReplyAt")]
public string LastReplyAt { get; set; }
/// <summary>
/// 最近一条公开留言的发送方类型。
/// </summary>
[DataMember(Name = "lastSenderType")]
public string LastSenderType { get; set; }
/// <summary>
/// 未读留言数量。
/// </summary>
[DataMember(Name = "unreadCount")]
public int UnreadCount { get; set; }
/// <summary>
/// 工单版本号,按接口返回的字符串原样保存。
/// </summary>
[DataMember(Name = "version")]
public string Version { get; set; }
}
/// <summary>
/// 订单留言板响应。
/// </summary>
[DataContract]
public sealed class WorkOrderMessagesResponse
{
/// <summary>
/// 请求是否成功。
/// </summary>
[DataMember(Name = "success")]
public bool Success { get; set; }
/// <summary>
/// 订单所属店铺 APPID。
/// </summary>
[DataMember(Name = "appId")]
public string AppId { get; set; }
/// <summary>
/// 微信订单号。
/// </summary>
[DataMember(Name = "wechatOrderId")]
public string WechatOrderId { get; set; }
/// <summary>
/// 当前页公开留言,按 sequenceNo 升序排列。
/// </summary>
[DataMember(Name = "items")]
public List<TicketMessageDto> Items { get; set; }
/// <summary>
/// 是否还有更早的留言。
/// </summary>
[DataMember(Name = "hasMore")]
public bool HasMore { get; set; }
/// <summary>
/// 加载更早留言时使用的 before 游标。
/// </summary>
[DataMember(Name = "nextBefore")]
public string NextBefore { get; set; }
/// <summary>
/// 接口返回的身份校验标记,当前恒为 false。
/// </summary>
[DataMember(Name = "identityVerified")]
public bool IdentityVerified { get; set; }
}
/// <summary>
/// 单条公开留言。
/// </summary>
[DataContract]
public sealed class TicketMessageDto
{
/// <summary>
/// 消息唯一 ID,用于去重。
/// </summary>
[DataMember(Name = "messageId")]
public string MessageId { get; set; }
/// <summary>
/// 工单内连续递增的消息序号。
/// </summary>
[DataMember(Name = "sequenceNo")]
public int SequenceNo { get; set; }
/// <summary>
/// 发送方类型,例如 customer。
/// </summary>
[DataMember(Name = "senderType")]
public string SenderType { get; set; }
/// <summary>
/// 发送方显示名称。
/// </summary>
[DataMember(Name = "senderName")]
public string SenderName { get; set; }
/// <summary>
/// 留言正文;消息撤回后为空字符串。
/// </summary>
[DataMember(Name = "text")]
public string Text { get; set; }
/// <summary>
/// 消息是否已撤回。
/// </summary>
[DataMember(Name = "recalled")]
public bool Recalled { get; set; }
/// <summary>
/// 消息发送时间,由接口以 ISO 8601 字符串传输。
/// </summary>
[DataMember(Name = "createdAt")]
public string CreatedAt { get; set; }
}
/// <summary>
/// 创建工单响应。
/// </summary>
[DataContract]
public sealed class CreateWorkOrderResponse
{
/// <summary>
/// 请求是否成功。
/// </summary>
[DataMember(Name = "success")]
public bool Success { get; set; }
/// <summary>
/// 创建结果,取值 created、existing 或 reopened。
/// </summary>
[DataMember(Name = "operation")]
public string Operation { get; set; }
/// <summary>
/// 订单所属店铺 APPID。
/// </summary>
[DataMember(Name = "appId")]
public string AppId { get; set; }
/// <summary>
/// 微信订单号。
/// </summary>
[DataMember(Name = "wechatOrderId")]
public string WechatOrderId { get; set; }
/// <summary>
/// 操作后的工单状态。
/// </summary>
[DataMember(Name = "status")]
public string Status { get; set; }
/// <summary>
/// 创建或重开时追加的首条消息 ID。
/// </summary>
[DataMember(Name = "messageId")]
public string MessageId { get; set; }
/// <summary>
/// 操作后的工单版本号字符串。
/// </summary>
[DataMember(Name = "version")]
public string Version { get; set; }
/// <summary>
/// 是否为幂等编号对应的历史回执。
/// </summary>
[DataMember(Name = "replayed")]
public bool Replayed { get; set; }
/// <summary>
/// 接口返回的身份校验标记,当前恒为 false。
/// </summary>
[DataMember(Name = "identityVerified")]
public bool IdentityVerified { get; set; }
}
/// <summary>
/// 提交客户留言响应。
/// </summary>
[DataContract]
public sealed class SendMessageResponse
{
/// <summary>
/// 请求是否成功。
/// </summary>
[DataMember(Name = "success")]
public bool Success { get; set; }
/// <summary>
/// 订单所属店铺 APPID。
/// </summary>
[DataMember(Name = "appId")]
public string AppId { get; set; }
/// <summary>
/// 微信订单号。
/// </summary>
[DataMember(Name = "wechatOrderId")]
public string WechatOrderId { get; set; }
/// <summary>
/// 服务端正式生成的消息对象。
/// </summary>
[DataMember(Name = "message")]
public TicketMessageDto Message { get; set; }
/// <summary>
/// 留言后的工单状态。
/// </summary>
[DataMember(Name = "status")]
public string Status { get; set; }
/// <summary>
/// 留言后的工单版本号字符串。
/// </summary>
[DataMember(Name = "version")]
public string Version { get; set; }
/// <summary>
/// 本次留言是否重新开启了已完结工单。
/// </summary>
[DataMember(Name = "reopened")]
public bool Reopened { get; set; }
/// <summary>
/// 是否为幂等编号对应的历史回执。
/// </summary>
[DataMember(Name = "replayed")]
public bool Replayed { get; set; }
/// <summary>
/// 接口返回的身份校验标记,当前恒为 false。
/// </summary>
[DataMember(Name = "identityVerified")]
public bool IdentityVerified { get; set; }
}
/// <summary>
/// 通用成功状态响应,仅用于在解析具体业务对象前检查顶层 success。
/// </summary>
[DataContract]
internal sealed class ApiSuccessResponse
{
/// <summary>
/// 请求是否成功。
/// </summary>
[DataMember(Name = "success")]
public bool Success { get; set; }
}
/// <summary>
/// 工单接口错误响应。
/// </summary>
[DataContract]
internal sealed class ApiErrorResponse
{
/// <summary>
/// 请求是否成功,错误响应通常为 false。
/// </summary>
[DataMember(Name = "success")]
public bool Success { get; set; }
/// <summary>
/// 可展示给用户的错误说明。
/// </summary>
[DataMember(Name = "message")]
public string Message { get; set; }
/// <summary>
/// 兼容旧接口的错误说明字段。
/// </summary>
[DataMember(Name = "error")]
public string Error { get; set; }
/// <summary>
/// 服务端错误编码。
/// </summary>
[DataMember(Name = "code")]
public string Code { get; set; }
}
@@ -0,0 +1,121 @@
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Globalization;
using System.Linq;
namespace OrderSupportTicket.Models;
/// <summary>
/// 工单页运行参数。APPID 与拍单账号由宿主程序注入,页面内不硬编码账号。
/// </summary>
public sealed class TicketClientOptions
{
public TicketClientOptions()
{
AppIds = new List<string>();
Orders = new List<TicketOrderOption>();
}
/// <summary>
/// 当前拍单账号,对应接口字段 purchaseAccount。
/// </summary>
public string UserName { get; set; }
/// <summary>
/// 当前账号需要订阅和查询的店铺 APPID 集合,最多 50 个。
/// </summary>
public IList<string> AppIds { get; set; }
/// <summary>
/// 可关联到新工单的本地订单集合。
/// </summary>
public IList<TicketOrderOption> Orders { get; set; }
public static TicketClientOptions FromApplicationSettings()
{
var purchaseAccount =
ConfigurationManager.AppSettings["CustomerWorkOrder.PurchaseAccount"];
var appIds = ConfigurationManager.AppSettings["CustomerWorkOrder.AppIds"];
return new TicketClientOptions
{
UserName = purchaseAccount,
AppIds = (appIds ?? string.Empty)
.Split(new[] { ',', ';', ' ' }, StringSplitOptions.RemoveEmptyEntries)
.Select(a => a.Trim())
.Where(a => a.Length > 0)
.Distinct(StringComparer.OrdinalIgnoreCase)
.ToList()
};
}
}
/// <summary>
/// 新建工单时需要在本地订单列表中关联的订单。
/// 微信订单号始终是字符串,禁止转换为 Number。
/// </summary>
public sealed class TicketOrderOption
{
/// <summary>
/// 订单所属店铺的 APPID,格式为 wx 加 16 位小写十六进制字符。
/// </summary>
public string AppId { get; set; }
/// <summary>
/// 微信订单号。接口要求保持为 10~64 位数字字符串,禁止转换为 Number。
/// </summary>
public string OrderId { get; set; }
/// <summary>
/// 商品名称,用于订单筛选和页面展示。
/// </summary>
public string GoodsName { get; set; }
/// <summary>
/// 订单金额,单位元,使用 decimal 保存原始金额,不在模型中保存格式化文本。
/// </summary>
public decimal Amount { get; set; }
/// <summary>
/// 订单下单时间。
/// </summary>
public DateTime OrderedAt { get; set; }
/// <summary>
/// 下拉列表显示文本,订单号在前、商品名在后,避免长商品名遮挡订单号。
/// </summary>
public string DisplayText
{
get
{
var product = string.IsNullOrWhiteSpace(GoodsName)
? "未命名商品"
: GoodsName;
return $"{OrderId} · {product}";
}
}
/// <summary>
/// 订单详情展示文本,包含商品名、金额和下单时间。
/// </summary>
public string ProductDescription
{
get
{
var product = string.IsNullOrWhiteSpace(GoodsName)
? "未提供商品名称"
: GoodsName;
var amount = "¥ " + Amount.ToString("N2", CultureInfo.GetCultureInfo("zh-CN"));
var orderedAt = OrderedAt == default
? "下单时间未知"
: OrderedAt.ToString("yyyy-MM-dd HH:mm", CultureInfo.CurrentCulture);
return $"商品:{product}\n金额:{amount} 下单时间:{orderedAt}";
}
}
/// <summary>
/// 当前工单的唯一订单键,由 APPID 和微信订单号共同组成。
/// </summary>
public string OrderKey => $"{AppId}|{OrderId}";
}
@@ -0,0 +1,133 @@
using System.Runtime.Serialization;
namespace OrderSupportTicket.Models;
/// <summary>
/// SSE 实时通知。通知只包含摘要定位信息,收到后需要重新查询对应工单。
/// </summary>
[DataContract]
public sealed class TicketRealtimeNotification
{
/// <summary>
/// 服务端事件 ID,用于至少一次投递时的去重。
/// </summary>
[DataMember(Name = "eventId")]
public string EventId { get; set; }
/// <summary>
/// 事件类型,例如 relay.ticket.message_added 或 resync_required。
/// </summary>
[DataMember(Name = "type")]
public string Type { get; set; }
/// <summary>
/// 发生变更的店铺 APPID。
/// </summary>
[DataMember(Name = "appId")]
public string AppId { get; set; }
/// <summary>
/// 发生变更的微信订单号,保持为数字字符串。
/// </summary>
[DataMember(Name = "wechatOrderId")]
public string WechatOrderId { get; set; }
/// <summary>
/// 新增消息的消息 ID。
/// </summary>
[DataMember(Name = "messageId")]
public string MessageId { get; set; }
/// <summary>
/// 工单版本号,按服务端返回的字符串原样保存。
/// </summary>
[DataMember(Name = "version")]
public string Version { get; set; }
/// <summary>
/// 事件发生时间,保留服务端 ISO 8601 字符串。
/// </summary>
[DataMember(Name = "occurredAt")]
public string OccurredAt { get; set; }
/// <summary>
/// 是否要求客户端执行全量重新同步。
/// </summary>
public bool IsResyncRequired =>
string.Equals(Type, "resync_required", System.StringComparison.OrdinalIgnoreCase);
}
/// <summary>
/// SSE 连接状态。
/// </summary>
public enum TicketRealtimeState
{
/// <summary>
/// 未连接。
/// </summary>
Disconnected,
/// <summary>
/// 正在建立连接。
/// </summary>
Connecting,
/// <summary>
/// 已连接并接收事件。
/// </summary>
Connected,
/// <summary>
/// 连接中断,正在自动重连。
/// </summary>
Reconnecting
}
/// <summary>
/// SSE 通知事件参数。
/// </summary>
public sealed class TicketRealtimeNotificationEventArgs : System.EventArgs
{
/// <summary>
/// 创建 SSE 通知事件参数。
/// </summary>
/// <param name="notification">收到的 SSE 通知。</param>
public TicketRealtimeNotificationEventArgs(TicketRealtimeNotification notification)
{
Notification = notification;
}
/// <summary>
/// 收到的 SSE 通知。
/// </summary>
public TicketRealtimeNotification Notification { get; }
}
/// <summary>
/// SSE 连接状态变化事件参数。
/// </summary>
public sealed class TicketRealtimeStateChangedEventArgs : System.EventArgs
{
/// <summary>
/// 创建 SSE 连接状态变化事件参数。
/// </summary>
/// <param name="state">变化后的连接状态。</param>
/// <param name="message">可选的连接状态说明。</param>
public TicketRealtimeStateChangedEventArgs(
TicketRealtimeState state,
string message = null)
{
State = state;
Message = message;
}
/// <summary>
/// 变化后的连接状态。
/// </summary>
public TicketRealtimeState State { get; }
/// <summary>
/// 连接状态说明。
/// </summary>
public string Message { get; }
}
@@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
namespace OrderSupportTicket.Models;
/// <summary>
/// 工单列表本地缓存,仅用于比较服务器列表是否有新数据。
/// </summary>
public sealed class WorkOrderListCache
{
/// <summary>
/// 缓存对应的登录账号。
/// </summary>
public string UserName { get; set; }
/// <summary>
/// 用于判断新消息的订单时间缓存项。
/// </summary>
public List<WorkOrderListCacheItem> Items { get; set; } =
new List<WorkOrderListCacheItem>();
}
/// <summary>
/// 单个订单的新消息比较信息。
/// </summary>
public sealed class WorkOrderListCacheItem
{
/// <summary>
/// 微信订单号。
/// </summary>
public string OrderId { get; set; }
/// <summary>
/// 最后一条公开消息时间,用于判断是否有任意新消息。
/// </summary>
public DateTimeOffset LastMessageAt { get; set; }
}