using AllBase;
using Newtonsoft.Json;
using Newtonsoft.Json.Serialization;
using OrderSupportTicket.Models;
using OrderSupportTicket.Models.Api;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using WxOrder.Framework.Commons;
namespace OrderSupportTicket.Services;
///
/// 拍单软件工单 API v2 客户端。对应 CLIENT-TICKET-API-V2 正式订单协议。
///
public sealed class TicketApiService : ITicketApiService
{
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);
private static readonly Regex OrderIdRegex =
new Regex(@"^[0-9]{10,64}$", RegexOptions.Compiled | RegexOptions.CultureInvariant);
private static readonly Regex PurchaseAccountRegex =
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)
{
ServerUrl = "http://localhost:5188";
}
}
///
/// 获取工单列表
///
///
///
///
///
///
///
///
public async Task GetWorkOrdersAsync(
TicketClientOptions options,
string wechatOrderId = null,
string status = null,
int limit = 20,
string cursor = null)
{
ValidateOptions(options);
if (!string.IsNullOrWhiteSpace(wechatOrderId))
ValidateWechatOrderId(wechatOrderId);
if (limit < 1 || limit > 100)
throw new ArgumentOutOfRangeException(nameof(limit), "limit 必须为 1~100。");
var request = new ListWorkOrdersRequest
{
PurchaseAccount = options.UserName,
AppIds = NormalizeAppIds(options.AppIds),
WechatOrderId = string.IsNullOrWhiteSpace(wechatOrderId) ? null : wechatOrderId,
Status = string.IsNullOrWhiteSpace(status) ? null : status,
Limit = limit,
Cursor = string.IsNullOrWhiteSpace(cursor) ? null : cursor
};
return await PostAsync(
ApiRoot + "/list",
request);
}
///
/// 获取工单消息
///
///
///
///
///
///
///
///
public async Task GetMessagesAsync(
TicketClientOptions options,
string appId,
string wechatOrderId,
string before = null,
int limit = 50)
{
ValidateOptions(options);
ValidateAppId(appId);
ValidateWechatOrderId(wechatOrderId);
if (limit < 1 || limit > 100)
throw new ArgumentOutOfRangeException(nameof(limit), "limit 必须为 1~100。");
var request = new GetMessagesRequest
{
PurchaseAccount = options.UserName,
AppId = appId,
WechatOrderId = wechatOrderId,
Limit = limit,
Before = string.IsNullOrWhiteSpace(before) ? null : before
};
var path = ApiRoot
+ "/messages";
return await PostAsync(path, request);
}
///
/// 创建工单
///
///
///
///
///
///
public async Task CreateWorkOrderAsync(
TicketClientOptions options,
TicketOrderOption order,
string description,
string clientRequestId = null)
{
ValidateOptions(options);
ValidateOrder(order);
ValidateDescription(description);
ValidateOperationId(clientRequestId, "clientRequestId", required: false);
var request = new CreateWorkOrderRequest
{
PurchaseAccount = options.UserName,
AppId = order.AppId,
WechatOrderId = order.OrderId,
Description = description,
ClientRequestId = string.IsNullOrWhiteSpace(clientRequestId)
? null
: clientRequestId
};
return await PostAsync(
ApiRoot,
request);
}
///
/// 发送工单消息
///
///
///
///
///
///
///
public async Task SendMessageAsync(
TicketClientOptions options,
WorkOrderSummaryDto workOrder,
string text,
string clientMessageId)
{
ValidateOptions(options);
if (workOrder == null)
throw new ArgumentNullException(nameof(workOrder));
ValidateAppId(workOrder.AppId);
ValidateWechatOrderId(workOrder.WechatOrderId);
ValidateMessageText(text);
ValidateOperationId(clientMessageId, "clientMessageId", required: true);
var request = new SendMessageRequest
{
PurchaseAccount = options.UserName,
AppId = workOrder.AppId,
ClientMessageId = clientMessageId,
Text = text
};
var path = ApiRoot
+ "/orders/"
+ Uri.EscapeDataString(workOrder.WechatOrderId)
+ "/messages";
return await PostAsync(path, request);
}
public async Task MarkReadAsync(
TicketClientOptions options,
string appId,
string wechatOrderId,
int throughSequenceNo)
{
ValidateOptions(options);
ValidateAppId(appId);
ValidateWechatOrderId(wechatOrderId);
if (throughSequenceNo < 1)
return;
var request = new MarkReadRequest
{
PurchaseAccount = options.UserName,
AppId = appId,
ThroughSequenceNo = throughSequenceNo
};
var path = ApiRoot
+ "/orders/"
+ Uri.EscapeDataString(wechatOrderId)
+ "/read";
var html = await PostBodyAsync(path, request);
EnsureSuccessBody(html);
}
public static void ValidateOptions(TicketClientOptions options)
{
if (options == null)
throw new ArgumentNullException(nameof(options));
if (string.IsNullOrWhiteSpace(options.UserName)
|| !PurchaseAccountRegex.IsMatch(options.UserName))
{
throw new ArgumentException(
"purchaseAccount 必填,且只能为 2~120 位中英文、数字或 _.@-。",
nameof(options));
}
NormalizeAppIds(options.AppIds);
}
public static void ValidateAppId(string appId)
{
if (string.IsNullOrWhiteSpace(appId) || !AppIdRegex.IsMatch(appId))
throw new ArgumentException(
"appId 必须为 wx 加 16 位小写十六进制字符。",
nameof(appId));
}
public static void ValidateWechatOrderId(string wechatOrderId)
{
if (string.IsNullOrWhiteSpace(wechatOrderId)
|| !OrderIdRegex.IsMatch(wechatOrderId))
{
throw new ArgumentException(
"wechatOrderId 必须保持为 10~64 位数字字符串。",
nameof(wechatOrderId));
}
}
public static void ValidateOrder(TicketOrderOption order)
{
if (order == null)
throw new ArgumentNullException(nameof(order));
ValidateAppId(order.AppId);
ValidateWechatOrderId(order.OrderId);
}
private static async Task PostAsync(
string path,
object request)
{
var html = await PostBodyAsync(path, request);
return DeserializeResponse(html);
}
private static async Task PostBodyAsync(
string path,
object request)
{
var url = BuildUri(path);
// 请求 DTO 使用 JsonProperty 驼峰字段,先序列化后走 GetHtml 的字符串重载。
var json = JsonConvert.SerializeObject(request, JsonSettings);
var html = await GetHtml.PostJsonAsync(url, json);
if (string.IsNullOrWhiteSpace(html))
throw RequestFailed("工单接口未返回数据。");
return html;
}
private static void EnsureSuccessBody(string html)
{
if (string.IsNullOrWhiteSpace(html))
throw RequestFailed("工单接口未返回数据。");
ApiSuccessResponse response;
try
{
response = JsonConvert.DeserializeObject(html, JsonSettings);
}
catch (JsonException)
{
throw RequestFailed(html);
}
if (response != null && !response.Success)
{
throw RequestFailed(
ExtractErrorMessage(html)
?? "工单接口请求失败。",
html);
}
}
private static T DeserializeResponse(string html)
{
EnsureSuccessBody(html);
try
{
var result = JsonConvert.DeserializeObject(html, JsonSettings);
if (result == null)
throw new JsonSerializationException("接口响应为空。");
return result;
}
catch (JsonException)
{
throw RequestFailed("工单接口返回了无法解析的数据。", html);
}
}
private static string ExtractErrorMessage(string body)
{
if (string.IsNullOrWhiteSpace(body))
return null;
try
{
var error = JsonConvert.DeserializeObject(body, JsonSettings);
return error == null
? null
: error.Message
?? error.Error
?? error.Code;
}
catch (JsonException)
{
return null;
}
}
private static string BuildUri(string path)
{
return ServerUrl + path;
}
private static TicketApiException RequestFailed(
string message,
string responseBody = null)
{
return new TicketApiException(
ExtractStatusCode(responseBody),
message,
responseBody);
}
private static int ExtractStatusCode(string responseBody)
{
if (string.IsNullOrWhiteSpace(responseBody))
return 0;
try
{
var error = JsonConvert.DeserializeObject(responseBody, JsonSettings);
if (error != null && int.TryParse(error.Code, out var code))
return code;
}
catch (JsonException)
{
// GetHtml 在网络异常时会返回普通错误文本,此时没有 HTTP 状态码。
}
return 0;
}
private static List NormalizeAppIds(IEnumerable appIds)
{
var normalized = (appIds ?? Enumerable.Empty())
.Where(a => !string.IsNullOrWhiteSpace(a))
.Select(a => a.Trim())
.Distinct(StringComparer.Ordinal)
.ToList();
if (normalized.Count == 0)
throw new ArgumentException("appIds 至少需要一个 APPID。", nameof(appIds));
if (normalized.Count > 50)
throw new ArgumentException("appIds 最多支持 50 个 APPID。", nameof(appIds));
foreach (var appId in normalized)
ValidateAppId(appId);
return normalized;
}
private static void ValidateDescription(string description)
{
if (string.IsNullOrWhiteSpace(description)
|| description.Length < 1
|| description.Length > 2000)
{
throw new ArgumentException("description 必须为 1~2000 字。", nameof(description));
}
}
private static void ValidateMessageText(string text)
{
if (string.IsNullOrWhiteSpace(text)
|| text.Length < 1
|| text.Length > 4000)
{
throw new ArgumentException("text 必须为 1~4000 字。", nameof(text));
}
}
private static void ValidateOperationId(
string value,
string parameterName,
bool required)
{
if (string.IsNullOrWhiteSpace(value))
{
if (required)
throw new ArgumentException(parameterName + " 必填。", parameterName);
return;
}
if (!OperationIdRegex.IsMatch(value))
{
throw new ArgumentException(
parameterName + " 必须为 1~128 位字母、数字或 _.:-。",
parameterName);
}
}
}