更新JSON格式化
增加剪切板helper
This commit is contained in:
1 parent
d3488c98f5
commit
a217a74868
7 files changed
+342
-156
No files matched your search
@@ -1,549 +1,536 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.Serialization;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace OrderSupportTicket.Models.Api;
|
||||
|
||||
/// <summary>
|
||||
/// 创建工单请求。
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
internal sealed class CreateWorkOrderRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 拍单账号。
|
||||
/// </summary>
|
||||
[DataMember(Name = "purchaseAccount")]
|
||||
////[JsonProperty("purchaseAccount")]
|
||||
public string PurchaseAccount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 订单所属店铺 APPID。
|
||||
/// </summary>
|
||||
[DataMember(Name = "appId")]
|
||||
////[JsonProperty("appId")]
|
||||
public string AppId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 微信订单号,保持为数字字符串。
|
||||
/// </summary>
|
||||
[DataMember(Name = "wechatOrderId")]
|
||||
////[JsonProperty("wechatOrderId")]
|
||||
public string WechatOrderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 首条问题描述,长度 1~2000 字。
|
||||
/// </summary>
|
||||
[DataMember(Name = "description")]
|
||||
////[JsonProperty("description")]
|
||||
public string Description { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 可选的创建幂等编号。
|
||||
/// </summary>
|
||||
[DataMember(Name = "clientRequestId", EmitDefaultValue = false)]
|
||||
////[JsonProperty("clientRequestId", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string ClientRequestId { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 工单摘要列表请求。
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
internal sealed class ListWorkOrdersRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 拍单账号。
|
||||
/// </summary>
|
||||
[DataMember(Name = "purchaseAccount")]
|
||||
////[JsonProperty("purchaseAccount")]
|
||||
public string PurchaseAccount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 要查询的店铺 APPID 数组。
|
||||
/// </summary>
|
||||
[DataMember(Name = "appIds")]
|
||||
////[JsonProperty("appIds")]
|
||||
public IList<string> AppIds { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 可选的微信订单号精确筛选条件。
|
||||
/// </summary>
|
||||
[DataMember(Name = "wechatOrderId", EmitDefaultValue = false)]
|
||||
////[JsonProperty("wechatOrderId", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string WechatOrderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 可选的工单状态筛选条件。
|
||||
/// </summary>
|
||||
[DataMember(Name = "status", EmitDefaultValue = false)]
|
||||
////[JsonProperty("status", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 本页最大返回数量,范围 1~100。
|
||||
/// </summary>
|
||||
[DataMember(Name = "limit")]
|
||||
////[JsonProperty("limit")]
|
||||
public int Limit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 下一页游标,使用服务端返回的原始值。
|
||||
/// </summary>
|
||||
[DataMember(Name = "cursor", EmitDefaultValue = false)]
|
||||
////[JsonProperty("cursor", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string Cursor { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 查询订单留言板请求。
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
internal sealed class GetMessagesRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 拍单账号。
|
||||
/// </summary>
|
||||
[DataMember(Name = "purchaseAccount")]
|
||||
////[JsonProperty("purchaseAccount")]
|
||||
public string PurchaseAccount { get; set; }
|
||||
/// <summary>
|
||||
/// 微信订单号,保持为数字字符串。
|
||||
/// </summary>
|
||||
[DataMember(Name = "wechatOrderId")]
|
||||
////[JsonProperty("wechatOrderId")]
|
||||
public string WechatOrderId { get; set; }
|
||||
/// <summary>
|
||||
/// 订单所属店铺 APPID。
|
||||
/// </summary>
|
||||
[DataMember(Name = "appId")]
|
||||
////[JsonProperty("appId")]
|
||||
public string AppId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 本页最大返回数量,范围 1~100。
|
||||
/// </summary>
|
||||
[DataMember(Name = "limit")]
|
||||
////[JsonProperty("limit")]
|
||||
public int Limit { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 加载更早留言时使用的 before 游标。
|
||||
/// </summary>
|
||||
[DataMember(Name = "before", EmitDefaultValue = false)]
|
||||
////[JsonProperty("before", NullValueHandling = NullValueHandling.Ignore)]
|
||||
public string Before { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 提交客户留言请求。
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
internal sealed class SendMessageRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 拍单账号。
|
||||
/// </summary>
|
||||
[DataMember(Name = "purchaseAccount")]
|
||||
////[JsonProperty("purchaseAccount")]
|
||||
public string PurchaseAccount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 订单所属店铺 APPID。
|
||||
/// </summary>
|
||||
[DataMember(Name = "appId")]
|
||||
////[JsonProperty("appId")]
|
||||
public string AppId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 留言幂等编号。
|
||||
/// </summary>
|
||||
[DataMember(Name = "clientMessageId")]
|
||||
////[JsonProperty("clientMessageId")]
|
||||
public string ClientMessageId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 留言正文,长度 1~4000 字。
|
||||
/// </summary>
|
||||
[DataMember(Name = "text")]
|
||||
////[JsonProperty("text")]
|
||||
public string Text { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 标记留言已读请求。
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
internal sealed class MarkReadRequest
|
||||
{
|
||||
/// <summary>
|
||||
/// 拍单账号。
|
||||
/// </summary>
|
||||
[DataMember(Name = "purchaseAccount")]
|
||||
////[JsonProperty("purchaseAccount")]
|
||||
public string PurchaseAccount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 订单所属店铺 APPID。
|
||||
/// </summary>
|
||||
[DataMember(Name = "appId")]
|
||||
////[JsonProperty("appId")]
|
||||
public string AppId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 已读到的最大留言序号。
|
||||
/// </summary>
|
||||
[DataMember(Name = "throughSequenceNo")]
|
||||
////[JsonProperty("throughSequenceNo")]
|
||||
public int ThroughSequenceNo { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 工单摘要分页响应。
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public sealed class WorkOrderListResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 请求是否成功。
|
||||
/// </summary>
|
||||
[DataMember(Name = "success")]
|
||||
////[JsonProperty("success")]
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前页工单摘要。
|
||||
/// </summary>
|
||||
[DataMember(Name = "items")]
|
||||
////[JsonProperty("items")]
|
||||
public List<WorkOrderSummaryDto> Items { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否还有下一页。
|
||||
/// </summary>
|
||||
[DataMember(Name = "hasMore")]
|
||||
////[JsonProperty("hasMore")]
|
||||
public bool HasMore { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 下一页游标。
|
||||
/// </summary>
|
||||
[DataMember(Name = "nextCursor")]
|
||||
////[JsonProperty("nextCursor")]
|
||||
public string NextCursor { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 接口返回的身份校验标记,当前恒为 false。
|
||||
/// </summary>
|
||||
[DataMember(Name = "identityVerified")]
|
||||
////[JsonProperty("identityVerified")]
|
||||
public bool IdentityVerified { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单个订单的工单摘要。
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public sealed class WorkOrderSummaryDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 订单所属店铺 APPID。
|
||||
/// </summary>
|
||||
[DataMember(Name = "appId")]
|
||||
////[JsonProperty("appId")]
|
||||
public string AppId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 微信订单号,保持为数字字符串。
|
||||
/// </summary>
|
||||
[DataMember(Name = "wechatOrderId")]
|
||||
////[JsonProperty("wechatOrderId")]
|
||||
public string WechatOrderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 工单状态编码。
|
||||
/// </summary>
|
||||
[DataMember(Name = "status")]
|
||||
////[JsonProperty("status")]
|
||||
public string Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 工单状态中文文本。
|
||||
/// </summary>
|
||||
[DataMember(Name = "statusText")]
|
||||
////[JsonProperty("statusText")]
|
||||
public string StatusText { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 工单创建时间,由接口以 ISO 8601 字符串传输。
|
||||
/// </summary>
|
||||
[DataMember(Name = "createdAt")]
|
||||
////[JsonProperty("createdAt")]
|
||||
public string CreatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 工单更新时间,由接口以 ISO 8601 字符串传输。
|
||||
/// </summary>
|
||||
[DataMember(Name = "updatedAt")]
|
||||
////[JsonProperty("updatedAt")]
|
||||
public string UpdatedAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最近一条公开留言时间,由接口以 ISO 8601 字符串传输。
|
||||
/// </summary>
|
||||
[DataMember(Name = "lastMessageAt")]
|
||||
////[JsonProperty("lastMessageAt")]
|
||||
public string LastMessageAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最近一次客服正式回复时间;从未回复时为 null。
|
||||
/// </summary>
|
||||
[DataMember(Name = "lastReplyAt")]
|
||||
////[JsonProperty("lastReplyAt")]
|
||||
public string LastReplyAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 最近一条公开留言的发送方类型。
|
||||
/// </summary>
|
||||
[DataMember(Name = "lastSenderType")]
|
||||
////[JsonProperty("lastSenderType")]
|
||||
public string LastSenderType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 未读留言数量。
|
||||
/// </summary>
|
||||
[DataMember(Name = "unreadCount")]
|
||||
////[JsonProperty("unreadCount")]
|
||||
public int UnreadCount { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 工单版本号,按接口返回的字符串原样保存。
|
||||
/// </summary>
|
||||
[DataMember(Name = "version")]
|
||||
////[JsonProperty("version")]
|
||||
public string Version { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 订单留言板响应。
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public sealed class WorkOrderMessagesResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 请求是否成功。
|
||||
/// </summary>
|
||||
[DataMember(Name = "success")]
|
||||
////[JsonProperty("success")]
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 订单所属店铺 APPID。
|
||||
/// </summary>
|
||||
[DataMember(Name = "appId")]
|
||||
////[JsonProperty("appId")]
|
||||
public string AppId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 微信订单号。
|
||||
/// </summary>
|
||||
[DataMember(Name = "wechatOrderId")]
|
||||
////[JsonProperty("wechatOrderId")]
|
||||
public string WechatOrderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 当前页公开留言,按 sequenceNo 升序排列。
|
||||
/// </summary>
|
||||
[DataMember(Name = "items")]
|
||||
////[JsonProperty("items")]
|
||||
public List<TicketMessageDto> Items { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否还有更早的留言。
|
||||
/// </summary>
|
||||
[DataMember(Name = "hasMore")]
|
||||
////[JsonProperty("hasMore")]
|
||||
public bool HasMore { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 加载更早留言时使用的 before 游标。
|
||||
/// </summary>
|
||||
[DataMember(Name = "nextBefore")]
|
||||
////[JsonProperty("nextBefore")]
|
||||
public string NextBefore { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 接口返回的身份校验标记,当前恒为 false。
|
||||
/// </summary>
|
||||
[DataMember(Name = "identityVerified")]
|
||||
////[JsonProperty("identityVerified")]
|
||||
public bool IdentityVerified { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 单条公开留言。
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public sealed class TicketMessageDto
|
||||
{
|
||||
/// <summary>
|
||||
/// 消息唯一 ID,用于去重。
|
||||
/// </summary>
|
||||
[DataMember(Name = "messageId")]
|
||||
////[JsonProperty("messageId")]
|
||||
public string MessageId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 工单内连续递增的消息序号。
|
||||
/// </summary>
|
||||
[DataMember(Name = "sequenceNo")]
|
||||
////[JsonProperty("sequenceNo")]
|
||||
public int SequenceNo { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 发送方类型,例如 customer。
|
||||
/// </summary>
|
||||
[DataMember(Name = "senderType")]
|
||||
////[JsonProperty("senderType")]
|
||||
public string SenderType { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 发送方显示名称。
|
||||
/// </summary>
|
||||
[DataMember(Name = "senderName")]
|
||||
////[JsonProperty("senderName")]
|
||||
public string SenderName { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 留言正文;消息撤回后为空字符串。
|
||||
/// </summary>
|
||||
[DataMember(Name = "text")]
|
||||
////[JsonProperty("text")]
|
||||
public string Text { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 消息是否已撤回。
|
||||
/// </summary>
|
||||
[DataMember(Name = "recalled")]
|
||||
////[JsonProperty("recalled")]
|
||||
public bool Recalled { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 消息发送时间,由接口以 ISO 8601 字符串传输。
|
||||
/// </summary>
|
||||
[DataMember(Name = "createdAt")]
|
||||
////[JsonProperty("createdAt")]
|
||||
public string CreatedAt { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建工单响应。
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public sealed class CreateWorkOrderResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 请求是否成功。
|
||||
/// </summary>
|
||||
[DataMember(Name = "success")]
|
||||
////[JsonProperty("success")]
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建结果,取值 created、existing 或 reopened。
|
||||
/// </summary>
|
||||
[DataMember(Name = "operation")]
|
||||
////[JsonProperty("operation")]
|
||||
public string Operation { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 订单所属店铺 APPID。
|
||||
/// </summary>
|
||||
[DataMember(Name = "appId")]
|
||||
////[JsonProperty("appId")]
|
||||
public string AppId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 微信订单号。
|
||||
/// </summary>
|
||||
[DataMember(Name = "wechatOrderId")]
|
||||
////[JsonProperty("wechatOrderId")]
|
||||
public string WechatOrderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 操作后的工单状态。
|
||||
/// </summary>
|
||||
[DataMember(Name = "status")]
|
||||
////[JsonProperty("status")]
|
||||
public string Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 创建或重开时追加的首条消息 ID。
|
||||
/// </summary>
|
||||
[DataMember(Name = "messageId")]
|
||||
////[JsonProperty("messageId")]
|
||||
public string MessageId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 操作后的工单版本号字符串。
|
||||
/// </summary>
|
||||
[DataMember(Name = "version")]
|
||||
////[JsonProperty("version")]
|
||||
public string Version { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否为幂等编号对应的历史回执。
|
||||
/// </summary>
|
||||
[DataMember(Name = "replayed")]
|
||||
////[JsonProperty("replayed")]
|
||||
public bool Replayed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 接口返回的身份校验标记,当前恒为 false。
|
||||
/// </summary>
|
||||
[DataMember(Name = "identityVerified")]
|
||||
////[JsonProperty("identityVerified")]
|
||||
public bool IdentityVerified { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 提交客户留言响应。
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public sealed class SendMessageResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 请求是否成功。
|
||||
/// </summary>
|
||||
[DataMember(Name = "success")]
|
||||
////[JsonProperty("success")]
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 订单所属店铺 APPID。
|
||||
/// </summary>
|
||||
[DataMember(Name = "appId")]
|
||||
////[JsonProperty("appId")]
|
||||
public string AppId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 微信订单号。
|
||||
/// </summary>
|
||||
[DataMember(Name = "wechatOrderId")]
|
||||
////[JsonProperty("wechatOrderId")]
|
||||
public string WechatOrderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 服务端正式生成的消息对象。
|
||||
/// </summary>
|
||||
[DataMember(Name = "message")]
|
||||
////[JsonProperty("message")]
|
||||
public TicketMessageDto Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 留言后的工单状态。
|
||||
/// </summary>
|
||||
[DataMember(Name = "status")]
|
||||
////[JsonProperty("status")]
|
||||
public string Status { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 留言后的工单版本号字符串。
|
||||
/// </summary>
|
||||
[DataMember(Name = "version")]
|
||||
////[JsonProperty("version")]
|
||||
public string Version { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 本次留言是否重新开启了已完结工单。
|
||||
/// </summary>
|
||||
[DataMember(Name = "reopened")]
|
||||
////[JsonProperty("reopened")]
|
||||
public bool Reopened { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否为幂等编号对应的历史回执。
|
||||
/// </summary>
|
||||
[DataMember(Name = "replayed")]
|
||||
////[JsonProperty("replayed")]
|
||||
public bool Replayed { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 接口返回的身份校验标记,当前恒为 false。
|
||||
/// </summary>
|
||||
[DataMember(Name = "identityVerified")]
|
||||
////[JsonProperty("identityVerified")]
|
||||
public bool IdentityVerified { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通用成功状态响应,仅用于在解析具体业务对象前检查顶层 success。
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
internal sealed class ApiSuccessResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 请求是否成功。
|
||||
/// </summary>
|
||||
[DataMember(Name = "success")]
|
||||
////[JsonProperty("success")]
|
||||
public bool Success { get; set; }
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 工单接口错误响应。
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
internal sealed class ApiErrorResponse
|
||||
{
|
||||
/// <summary>
|
||||
/// 请求是否成功,错误响应通常为 false。
|
||||
/// </summary>
|
||||
[DataMember(Name = "success")]
|
||||
////[JsonProperty("success")]
|
||||
public bool Success { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 可展示给用户的错误说明。
|
||||
/// </summary>
|
||||
[DataMember(Name = "message")]
|
||||
////[JsonProperty("message")]
|
||||
public string Message { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 兼容旧接口的错误说明字段。
|
||||
/// </summary>
|
||||
[DataMember(Name = "error")]
|
||||
////[JsonProperty("error")]
|
||||
public string Error { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 服务端错误编码。
|
||||
/// </summary>
|
||||
[DataMember(Name = "code")]
|
||||
////[JsonProperty("code")]
|
||||
public string Code { get; set; }
|
||||
}
|
||||
@@ -1,58 +1,58 @@
|
||||
using System.Runtime.Serialization;
|
||||
using Newtonsoft.Json;
|
||||
|
||||
namespace OrderSupportTicket.Models;
|
||||
|
||||
/// <summary>
|
||||
/// SSE 实时通知。通知只包含摘要定位信息,收到后需要重新查询对应工单。
|
||||
/// </summary>
|
||||
[DataContract]
|
||||
public sealed class TicketRealtimeNotification
|
||||
{
|
||||
/// <summary>
|
||||
/// 服务端事件 ID,用于至少一次投递时的去重。
|
||||
/// </summary>
|
||||
[DataMember(Name = "eventId")]
|
||||
////[JsonProperty("eventId")]
|
||||
public string EventId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 事件类型,例如 relay.ticket.message_added 或 resync_required。
|
||||
/// </summary>
|
||||
[DataMember(Name = "type")]
|
||||
////[JsonProperty("type")]
|
||||
public string Type { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 发生变更的店铺 APPID。
|
||||
/// </summary>
|
||||
[DataMember(Name = "appId")]
|
||||
////[JsonProperty("appId")]
|
||||
public string AppId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 发生变更的微信订单号,保持为数字字符串。
|
||||
/// </summary>
|
||||
[DataMember(Name = "wechatOrderId")]
|
||||
////[JsonProperty("wechatOrderId")]
|
||||
public string WechatOrderId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 新增消息的消息 ID。
|
||||
/// </summary>
|
||||
[DataMember(Name = "messageId")]
|
||||
////[JsonProperty("messageId")]
|
||||
public string MessageId { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 工单版本号,按服务端返回的字符串原样保存。
|
||||
/// </summary>
|
||||
[DataMember(Name = "version")]
|
||||
////[JsonProperty("version")]
|
||||
public string Version { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 事件发生时间,保留服务端 ISO 8601 字符串。
|
||||
/// </summary>
|
||||
[DataMember(Name = "occurredAt")]
|
||||
////[JsonProperty("occurredAt")]
|
||||
public string OccurredAt { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// 是否要求客户端执行全量重新同步。
|
||||
/// </summary>
|
||||
[JsonIgnore]
|
||||
public bool IsResyncRequired =>
|
||||
string.Equals(Type, "resync_required", System.StringComparison.OrdinalIgnoreCase);
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<!--<PackageReference Include="LaunchDarkly.EventSource" Version="6.0.0" />-->
|
||||
<PackageReference Include="WPFCommon" Version="1.2.0" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -23,7 +22,6 @@
|
||||
<ItemGroup>
|
||||
<Reference Include="System.Configuration" />
|
||||
<Reference Include="System.Net.Http" />
|
||||
<Reference Include="System.Runtime.Serialization" />
|
||||
<Reference Include="NaiBa.Util">
|
||||
<HintPath>..\DllReference\NaiBa.Util.dll</HintPath>
|
||||
<Private>true</Private>
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
using LaunchDarkly.EventSource;
|
||||
using Newtonsoft.Json;
|
||||
using OrderSupportTicket.Models;
|
||||
using OrderSupportTicket.Models.Api;
|
||||
using System;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Runtime.Serialization.Json;
|
||||
using System.Text;
|
||||
using System.Threading.Tasks;
|
||||
|
||||
namespace OrderSupportTicket.Services;
|
||||
@@ -134,14 +132,9 @@ public sealed class EventSourceTicketRealtimeService : ITicketRealtimeService
|
||||
TicketRealtimeNotification notification;
|
||||
try
|
||||
{
|
||||
var serializer = new DataContractJsonSerializer(
|
||||
typeof(TicketRealtimeNotification));
|
||||
using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(data)))
|
||||
{
|
||||
notification = (TicketRealtimeNotification)serializer.ReadObject(stream);
|
||||
}
|
||||
notification = JsonConvert.DeserializeObject<TicketRealtimeNotification>(data, TicketApiService.JsonSettings);
|
||||
}
|
||||
catch (SerializationException)
|
||||
catch (JsonException)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,13 +1,11 @@
|
||||
using AllBase;
|
||||
using Newtonsoft.Json;
|
||||
using Newtonsoft.Json.Serialization;
|
||||
using OrderSupportTicket.Models;
|
||||
using OrderSupportTicket.Models.Api;
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Runtime.Serialization;
|
||||
using System.Runtime.Serialization.Json;
|
||||
using System.Text;
|
||||
using System.Text.RegularExpressions;
|
||||
using System.Threading.Tasks;
|
||||
using WxOrder.Framework.Commons;
|
||||
@@ -29,6 +27,11 @@ 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);
|
||||
internal static readonly JsonSerializerSettings JsonSettings = new JsonSerializerSettings
|
||||
{
|
||||
NullValueHandling = NullValueHandling.Ignore,
|
||||
ContractResolver = new CamelCasePropertyNamesContractResolver()
|
||||
};
|
||||
static TicketApiService()
|
||||
{
|
||||
if (EnvironmentHelper.IsTest)
|
||||
@@ -259,8 +262,8 @@ public sealed class TicketApiService : ITicketApiService
|
||||
object request)
|
||||
{
|
||||
var url = BuildUri(path);
|
||||
// 请求 DTO 使用 DataMember 驼峰字段,先序列化后走 GetHtml 的字符串重载。
|
||||
var json = Serialize(request);
|
||||
// 请求 DTO 使用 JsonProperty 驼峰字段,先序列化后走 GetHtml 的字符串重载。
|
||||
var json = JsonConvert.SerializeObject(request, JsonSettings);
|
||||
var html = await GetHtml.PostJsonAsync(url, json);
|
||||
if (string.IsNullOrWhiteSpace(html))
|
||||
throw RequestFailed("工单接口未返回数据。");
|
||||
@@ -275,9 +278,9 @@ public sealed class TicketApiService : ITicketApiService
|
||||
ApiSuccessResponse response;
|
||||
try
|
||||
{
|
||||
response = Deserialize<ApiSuccessResponse>(html);
|
||||
response = JsonConvert.DeserializeObject<ApiSuccessResponse>(html, JsonSettings);
|
||||
}
|
||||
catch (SerializationException)
|
||||
catch (JsonException)
|
||||
{
|
||||
throw RequestFailed(html);
|
||||
}
|
||||
@@ -296,12 +299,12 @@ public sealed class TicketApiService : ITicketApiService
|
||||
EnsureSuccessBody(html);
|
||||
try
|
||||
{
|
||||
var result = Deserialize<T>(html);
|
||||
var result = JsonConvert.DeserializeObject<T>(html, JsonSettings);
|
||||
if (result == null)
|
||||
throw new SerializationException("接口响应为空。");
|
||||
throw new JsonSerializationException("接口响应为空。");
|
||||
return result;
|
||||
}
|
||||
catch (SerializationException)
|
||||
catch (JsonException)
|
||||
{
|
||||
throw RequestFailed("工单接口返回了无法解析的数据。", html);
|
||||
}
|
||||
@@ -314,38 +317,19 @@ public sealed class TicketApiService : ITicketApiService
|
||||
|
||||
try
|
||||
{
|
||||
var error = Deserialize<ApiErrorResponse>(body);
|
||||
var error = JsonConvert.DeserializeObject<ApiErrorResponse>(body, JsonSettings);
|
||||
return error == null
|
||||
? null
|
||||
: error.Message
|
||||
?? error.Error
|
||||
?? error.Code;
|
||||
}
|
||||
catch (SerializationException)
|
||||
catch (JsonException)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
private static string Serialize(object value)
|
||||
{
|
||||
var serializer = new DataContractJsonSerializer(value.GetType());
|
||||
using (var stream = new MemoryStream())
|
||||
{
|
||||
serializer.WriteObject(stream, value);
|
||||
return Encoding.UTF8.GetString(stream.ToArray());
|
||||
}
|
||||
}
|
||||
|
||||
private static T Deserialize<T>(string json)
|
||||
{
|
||||
var serializer = new DataContractJsonSerializer(typeof(T));
|
||||
using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(json ?? string.Empty)))
|
||||
{
|
||||
return (T)serializer.ReadObject(stream);
|
||||
}
|
||||
}
|
||||
|
||||
private static string BuildUri(string path)
|
||||
{
|
||||
return ServerUrl + path;
|
||||
@@ -368,11 +352,11 @@ public sealed class TicketApiService : ITicketApiService
|
||||
|
||||
try
|
||||
{
|
||||
var error = Deserialize<ApiErrorResponse>(responseBody);
|
||||
var error = JsonConvert.DeserializeObject<ApiErrorResponse>(responseBody, JsonSettings);
|
||||
if (error != null && int.TryParse(error.Code, out var code))
|
||||
return code;
|
||||
}
|
||||
catch (SerializationException)
|
||||
catch (JsonException)
|
||||
{
|
||||
// GetHtml 在网络异常时会返回普通错误文本,此时没有 HTTP 状态码。
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ using _29WxOrderWpf.ToolCtrls;
|
||||
using OrderSupportTicket.Models;
|
||||
using OrderSupportTicket.Services;
|
||||
using OrderSupportTicket.ViewModels;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
@@ -154,16 +155,15 @@ public partial class OrderSupportTicketView : UserControl
|
||||
if (window == null)
|
||||
return;
|
||||
|
||||
try
|
||||
{
|
||||
Clipboard.SetText(orderId);
|
||||
ToastHelper.Show(window, "订单号已复制");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
ToastHelper.Show(window, "复制失败:" + ex.Message, ToastType.Error);
|
||||
}
|
||||
}
|
||||
if (ClipboardHelper.TrySetText(orderId))
|
||||
{
|
||||
ToastHelper.Show(window, "订单号已复制");
|
||||
}
|
||||
else
|
||||
{
|
||||
ToastHelper.Show(window, "复制失败");
|
||||
}
|
||||
}
|
||||
|
||||
private async void OnLoaded(object sender, System.Windows.RoutedEventArgs e)
|
||||
{
|
||||
|
||||
@@ -0,0 +1,224 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using SysClipboard = System.Windows.Clipboard;
|
||||
|
||||
/// <summary>
|
||||
/// 剪贴板帮助类(WinForms / WPF 通用)
|
||||
/// 解决的问题:
|
||||
/// 1. CLIPBRD_E_CANT_OPEN (0x800401D0) 等 COMException / ExternalException
|
||||
/// (剪贴板被其他进程占用,如远程桌面、剪贴板管理器、杀毒软件、微信/QQ 等)
|
||||
/// 2. 非 STA 线程调用导致的 ThreadStateException
|
||||
/// 3. 空字符串 / null 导致的 ArgumentNullException
|
||||
/// </summary>
|
||||
public static class ClipboardHelper
|
||||
{
|
||||
/// <summary>默认重试次数</summary>
|
||||
public static int DefaultRetryCount { get; set; } = 10;
|
||||
|
||||
/// <summary>默认重试间隔(毫秒)</summary>
|
||||
public static int DefaultRetryDelayMs { get; set; } = 100;
|
||||
|
||||
#region 同步 API
|
||||
|
||||
/// <summary>复制文本,失败返回 false,不抛异常</summary>
|
||||
public static bool TrySetText(string text, int retryCount = -1, int retryDelayMs = -1)
|
||||
{
|
||||
return TryExecute(() =>
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
SysClipboard.Clear();
|
||||
else
|
||||
SetDataObjectCore(text);
|
||||
return true;
|
||||
}, out _, retryCount, retryDelayMs);
|
||||
}
|
||||
|
||||
/// <summary>复制任意数据(文本、文件列表、图片、DataObject 等)</summary>
|
||||
public static bool TrySetData(object data, int retryCount = -1, int retryDelayMs = -1)
|
||||
{
|
||||
if (data == null) return false;
|
||||
return TryExecute(() =>
|
||||
{
|
||||
SetDataObjectCore(data);
|
||||
return true;
|
||||
}, out _, retryCount, retryDelayMs);
|
||||
}
|
||||
|
||||
/// <summary>读取文本,失败或无文本返回 null</summary>
|
||||
public static string TryGetText(int retryCount = -1, int retryDelayMs = -1)
|
||||
{
|
||||
TryExecute(() => SysClipboard.ContainsText() ? SysClipboard.GetText() : null,
|
||||
out string result, retryCount, retryDelayMs);
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>清空剪贴板</summary>
|
||||
public static bool TryClear(int retryCount = -1, int retryDelayMs = -1)
|
||||
{
|
||||
return TryExecute(() => { SysClipboard.Clear(); return true; },
|
||||
out _, retryCount, retryDelayMs);
|
||||
}
|
||||
|
||||
/// <summary>复制文本,失败时抛出最后一次的异常</summary>
|
||||
public static void SetText(string text, int retryCount = -1, int retryDelayMs = -1)
|
||||
{
|
||||
Execute(() =>
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
SysClipboard.Clear();
|
||||
else
|
||||
SetDataObjectCore(text);
|
||||
return true;
|
||||
}, retryCount, retryDelayMs);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 异步 API(不阻塞 UI 线程,建议在 UI 线程 await 调用)
|
||||
|
||||
public static async Task<bool> TrySetTextAsync(string text, int retryCount = -1, int retryDelayMs = -1)
|
||||
{
|
||||
retryCount = retryCount < 0 ? DefaultRetryCount : retryCount;
|
||||
retryDelayMs = retryDelayMs < 0 ? DefaultRetryDelayMs : retryDelayMs;
|
||||
|
||||
for (int i = 0; i < retryCount; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
RunSta(() =>
|
||||
{
|
||||
if (string.IsNullOrEmpty(text))
|
||||
SysClipboard.Clear();
|
||||
else
|
||||
SetDataObjectCore(text);
|
||||
return true;
|
||||
});
|
||||
return true;
|
||||
}
|
||||
catch (Exception ex) when (IsRetryable(ex))
|
||||
{
|
||||
if (i < retryCount - 1)
|
||||
await Task.Delay(retryDelayMs).ConfigureAwait(true); // 回到 UI 线程
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
public static async Task<string> TryGetTextAsync(int retryCount = -1, int retryDelayMs = -1)
|
||||
{
|
||||
retryCount = retryCount < 0 ? DefaultRetryCount : retryCount;
|
||||
retryDelayMs = retryDelayMs < 0 ? DefaultRetryDelayMs : retryDelayMs;
|
||||
|
||||
for (int i = 0; i < retryCount; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
return RunSta(() => SysClipboard.ContainsText() ? SysClipboard.GetText() : null);
|
||||
}
|
||||
catch (Exception ex) when (IsRetryable(ex))
|
||||
{
|
||||
if (i < retryCount - 1)
|
||||
await Task.Delay(retryDelayMs).ConfigureAwait(true);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region 内部实现
|
||||
|
||||
private static void SetDataObjectCore(object data)
|
||||
{
|
||||
SysClipboard.SetDataObject(data, true);
|
||||
}
|
||||
|
||||
private static T Execute<T>(Func<T> action, int retryCount, int retryDelayMs)
|
||||
{
|
||||
retryCount = retryCount < 0 ? DefaultRetryCount : retryCount;
|
||||
retryDelayMs = retryDelayMs < 0 ? DefaultRetryDelayMs : retryDelayMs;
|
||||
if (retryCount < 1) retryCount = 1;
|
||||
|
||||
for (int i = 0; ; i++)
|
||||
{
|
||||
try
|
||||
{
|
||||
return RunSta(action);
|
||||
}
|
||||
catch (Exception ex) when (IsRetryable(ex) && i < retryCount - 1)
|
||||
{
|
||||
Thread.Sleep(retryDelayMs);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryExecute<T>(Func<T> action, out T result, int retryCount, int retryDelayMs)
|
||||
{
|
||||
try
|
||||
{
|
||||
result = Execute(action, retryCount, retryDelayMs);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
result = default(T);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>剪贴板必须在 STA 线程访问;当前不是 STA 就新建一个 STA 线程执行</summary>
|
||||
private static T RunSta<T>(Func<T> action)
|
||||
{
|
||||
if (Thread.CurrentThread.GetApartmentState() == ApartmentState.STA)
|
||||
return action();
|
||||
|
||||
T result = default(T);
|
||||
Exception error = null;
|
||||
var thread = new Thread(() =>
|
||||
{
|
||||
try { result = action(); }
|
||||
catch (Exception ex) { error = ex; }
|
||||
});
|
||||
thread.SetApartmentState(ApartmentState.STA);
|
||||
thread.Start();
|
||||
thread.Join();
|
||||
|
||||
if (error != null)
|
||||
{
|
||||
// 保留原始异常类型,便于上层判断
|
||||
System.Runtime.ExceptionServices.ExceptionDispatchInfo.Capture(error).Throw();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断是否为"剪贴板暂时被占用"类的可重试异常
|
||||
/// COMException 继承自 ExternalException,所以一起覆盖
|
||||
/// </summary>
|
||||
private static bool IsRetryable(Exception ex)
|
||||
{
|
||||
if (ex is ExternalException)
|
||||
{
|
||||
// 0x800401D0 CLIPBRD_E_CANT_OPEN
|
||||
// 0x800401D1 CLIPBRD_E_CANT_EMPTY
|
||||
// 0x800401D2 CLIPBRD_E_CANT_SET
|
||||
// 0x800401D3 CLIPBRD_E_BAD_DATA
|
||||
// 0x800401D4 CLIPBRD_E_CANT_CLOSE
|
||||
// WinForms 的 "Requested Clipboard operation did not succeed" 也是 ExternalException
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
Reference in new issue
Block a user