740 lines
24 KiB
C#
740 lines
24 KiB
C#
using System.Globalization;
|
||
using System.Net.Http.Headers;
|
||
using System.Security.Cryptography;
|
||
using System.Text;
|
||
using System.Text.Json;
|
||
using System.Text.Json.Serialization;
|
||
|
||
namespace SimpleAdmin.Application;
|
||
|
||
/// <summary>
|
||
/// 直接调用契约锁开放平台,不依赖旧版 .NET Framework SDK。
|
||
/// </summary>
|
||
public class QiyuesuoClient : IQiyuesuoClient
|
||
{
|
||
private const string SignatureHmacSha256 = "HMACSHA256";
|
||
private const string AccessTokenHeader = "x-qys-open-accesstoken";
|
||
private const string TimestampHeader = "x-qys-open-timestamp";
|
||
private const string NonceHeader = "x-qys-open-nonce";
|
||
private const string SignatureHeader = "x-qys-open-signature";
|
||
private const string SignatureVersionHeader = "x-qys-open-signature-version";
|
||
private const string RequestBodySignatureKey = "x-qys-open-body";
|
||
private static readonly HttpClient HttpClient = new()
|
||
{
|
||
Timeout = Timeout.InfiniteTimeSpan
|
||
};
|
||
|
||
private static readonly JsonSerializerOptions JsonOptions = CreateJsonOptions();
|
||
|
||
private readonly QiyuesuoSettingOptions _setting;
|
||
|
||
public QiyuesuoClient()
|
||
{
|
||
_setting = App.GetConfig<QiyuesuoSettingOptions>(
|
||
"QiyuesuoSetting",
|
||
true)
|
||
?? new QiyuesuoSettingOptions();
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<QiyuesuoContractResult> CreateDraft(
|
||
QiyuesuoCreateDraftInput input)
|
||
{
|
||
if (!long.TryParse(input.SealId, out var sealId))
|
||
throw Oops.Bah("契约锁 SealId 配置格式不正确");
|
||
|
||
var body = new
|
||
{
|
||
bizId = input.BizId,
|
||
subject = input.Subject,
|
||
tenantName = input.CompanyName,
|
||
description = "OnboardX 入职电子签",
|
||
send = false,
|
||
callbackUrl = NullIfBlank(input.CallbackUrl),
|
||
category = new
|
||
{
|
||
id = input.CategoryId
|
||
},
|
||
creator = new
|
||
{
|
||
name = input.CreatorName,
|
||
contact = input.CreatorMobile,
|
||
contactType = "MOBILE"
|
||
},
|
||
signatories = new object[]
|
||
{
|
||
new
|
||
{
|
||
tenantType = "PERSONAL",
|
||
tenantName = input.EmployeeName,
|
||
receiver = new
|
||
{
|
||
name = input.EmployeeName,
|
||
contact = input.EmployeeMobile,
|
||
contactType = "MOBILE"
|
||
},
|
||
serialNo = 1,
|
||
signatoryNo = "employee",
|
||
actions = new object[]
|
||
{
|
||
new
|
||
{
|
||
type = "PERSONAL",
|
||
name = "员工本人签署",
|
||
serialNo = 0,
|
||
actionNo = "employee_sign"
|
||
}
|
||
}
|
||
},
|
||
new
|
||
{
|
||
tenantType = "COMPANY",
|
||
tenantName = input.CompanyName,
|
||
receiver = new
|
||
{
|
||
name = input.CreatorName,
|
||
contact = input.CreatorMobile,
|
||
contactType = "MOBILE"
|
||
},
|
||
serialNo = 2,
|
||
signatoryNo = "company",
|
||
actions = new object[]
|
||
{
|
||
new
|
||
{
|
||
type = "COMPANY",
|
||
name = "企业人工盖章",
|
||
serialNo = 0,
|
||
autoSign = false,
|
||
sealId = input.SealId,
|
||
corpSealIds = new[] { sealId },
|
||
actionNo = "company_seal"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
};
|
||
|
||
return await PostJson<QiyuesuoContractResult>(
|
||
"/v2/contract/draft",
|
||
body);
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<QiyuesuoDocumentResult> AddDocument(
|
||
QiyuesuoAddDocumentInput input)
|
||
{
|
||
var fields = new Dictionary<string, string>
|
||
{
|
||
["contractId"] = input.ContractId,
|
||
["bizId"] = input.BizId,
|
||
["title"] = input.Title,
|
||
["fileSuffix"] = "docx",
|
||
["documentSort"] = input.DocumentSort.ToString(
|
||
CultureInfo.InvariantCulture)
|
||
};
|
||
return await PostFile<QiyuesuoDocumentResult>(
|
||
"/v2/document/addbyfile",
|
||
fields,
|
||
input.FileBytes,
|
||
input.FileName);
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public Task<QiyuesuoContractResult> Send(
|
||
QiyuesuoSendContractInput input)
|
||
{
|
||
return PostJson<QiyuesuoContractResult>(
|
||
"/v2/contract/send",
|
||
new
|
||
{
|
||
contractId = input.ContractId,
|
||
bizId = input.BizId,
|
||
tenantName = input.CompanyName,
|
||
locateAllStamperKeywords = false,
|
||
stampers = input.Stampers
|
||
});
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<string> GetSignPageUrl(
|
||
QiyuesuoSignPageInput input)
|
||
{
|
||
var result = await PostJsonElement(
|
||
"/v2/contract/pageurl",
|
||
new
|
||
{
|
||
contractId = input.ContractId,
|
||
user = new
|
||
{
|
||
contact = input.EmployeeMobile,
|
||
contactType = "MOBILE"
|
||
},
|
||
callbackPage = NullIfBlank(input.CallbackPage)
|
||
});
|
||
|
||
if (result.ValueKind == JsonValueKind.String)
|
||
return result.GetString();
|
||
if (TryGetString(result, "url", out var url)
|
||
|| TryGetString(result, "pageUrl", out url)
|
||
|| TryGetString(result, "signUrl", out url))
|
||
{
|
||
return url;
|
||
}
|
||
|
||
throw Oops.Bah("契约锁未返回员工签署页面地址");
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task<string> GetViewPageUrl(
|
||
QiyuesuoViewPageInput input)
|
||
{
|
||
var result = await GetJsonElement(
|
||
"/v2/contract/viewurl",
|
||
new Dictionary<string, string>
|
||
{
|
||
["contractId"] = input.ContractId,
|
||
["documentId"] = input.DocumentId,
|
||
["tenantName"] = input.CompanyName,
|
||
["downloadButton"] = "false",
|
||
["printButton"] = "false"
|
||
});
|
||
|
||
if (result.ValueKind == JsonValueKind.String)
|
||
return result.GetString();
|
||
if (TryGetString(result, "url", out var url)
|
||
|| TryGetString(result, "viewUrl", out url)
|
||
|| TryGetString(result, "pageUrl", out url))
|
||
{
|
||
return url;
|
||
}
|
||
|
||
throw Oops.Bah("契约锁未返回合同浏览页面地址");
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public Task<QiyuesuoContractResult> GetContractDetail(string contractId)
|
||
{
|
||
return GetJson<QiyuesuoContractResult>(
|
||
"/v2/contract/detail",
|
||
new Dictionary<string, string>
|
||
{
|
||
["contractId"] = contractId,
|
||
["queryActionOperate"] = "true",
|
||
["queryActionOperator"] = "true",
|
||
["queryLocation"] = "true"
|
||
});
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task Notice(string contractId)
|
||
{
|
||
await PostJsonElement(
|
||
"/v2/contract/notice",
|
||
new
|
||
{
|
||
contractId,
|
||
onlySigning = true
|
||
});
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task CompanySign(QiyuesuoCompanySignInput input)
|
||
{
|
||
await PostJsonElement(
|
||
"/v2/contract/companysign",
|
||
new
|
||
{
|
||
contractId = input.ContractId,
|
||
tenantName = input.CompanyName,
|
||
sealId = input.SealId
|
||
});
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public async Task Invalid(string contractId, string sealId, string reason)
|
||
{
|
||
await PostJsonElement(
|
||
"/v2/contract/invalid",
|
||
new
|
||
{
|
||
contractId,
|
||
sealId,
|
||
reason,
|
||
deleteDoc = false,
|
||
autoSign = true
|
||
});
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public Task<byte[]> DownloadDocument(string documentId)
|
||
{
|
||
return Download(
|
||
"/v2/document/download",
|
||
new Dictionary<string, string>
|
||
{
|
||
["documentId"] = documentId
|
||
});
|
||
}
|
||
|
||
/// <inheritdoc />
|
||
public Task<byte[]> DownloadArchive(string contractId)
|
||
{
|
||
return Download(
|
||
"/v2/contract/download",
|
||
new Dictionary<string, string>
|
||
{
|
||
["contractId"] = contractId,
|
||
["downloadItems"] = "CONTRACT,SIGNLOG",
|
||
["needCompressForOneFile"] = "true"
|
||
});
|
||
}
|
||
|
||
private async Task<T> PostJson<T>(string path, object body)
|
||
{
|
||
var result = await PostJsonElement(path, body);
|
||
return result.Deserialize<T>(JsonOptions)
|
||
?? throw Oops.Bah("契约锁返回数据为空");
|
||
}
|
||
|
||
private async Task<JsonElement> PostJsonElement(
|
||
string path,
|
||
object body)
|
||
{
|
||
ValidateSettings();
|
||
var json = JsonSerializer.Serialize(body, JsonOptions);
|
||
using var request = new HttpRequestMessage(
|
||
HttpMethod.Post,
|
||
BuildUrl(path));
|
||
request.Content = new StringContent(
|
||
json,
|
||
Encoding.UTF8,
|
||
"application/json");
|
||
AddAuthorizationHeaders(
|
||
request,
|
||
path,
|
||
HttpMethod.Post,
|
||
jsonBody: json);
|
||
return await SendForJsonResult(request);
|
||
}
|
||
|
||
private async Task<T> PostFile<T>(
|
||
string path,
|
||
Dictionary<string, string> fields,
|
||
byte[] fileBytes,
|
||
string fileName)
|
||
{
|
||
ValidateSettings();
|
||
using var content = new MultipartFormDataContent();
|
||
foreach (var field in fields)
|
||
{
|
||
content.Add(
|
||
new StringContent(field.Value ?? string.Empty, Encoding.UTF8),
|
||
field.Key);
|
||
}
|
||
|
||
var fileContent = new ByteArrayContent(fileBytes);
|
||
fileContent.Headers.ContentType = new MediaTypeHeaderValue(
|
||
"application/vnd.openxmlformats-officedocument.wordprocessingml.document");
|
||
content.Add(fileContent, "file", fileName);
|
||
|
||
using var request = new HttpRequestMessage(
|
||
HttpMethod.Post,
|
||
BuildUrl(path))
|
||
{
|
||
Content = content
|
||
};
|
||
AddAuthorizationHeaders(
|
||
request,
|
||
path,
|
||
HttpMethod.Post,
|
||
fields,
|
||
fileBytes);
|
||
var result = await SendForJsonResult(request);
|
||
return result.Deserialize<T>(JsonOptions)
|
||
?? throw Oops.Bah("契约锁返回数据为空");
|
||
}
|
||
|
||
private async Task<T> GetJson<T>(
|
||
string path,
|
||
Dictionary<string, string> query)
|
||
{
|
||
var result = await GetJsonElement(path, query);
|
||
return result.Deserialize<T>(JsonOptions)
|
||
?? throw Oops.Bah("契约锁返回数据为空");
|
||
}
|
||
|
||
private async Task<JsonElement> GetJsonElement(
|
||
string path,
|
||
Dictionary<string, string> query)
|
||
{
|
||
ValidateSettings();
|
||
using var request = new HttpRequestMessage(
|
||
HttpMethod.Get,
|
||
BuildUrl(path, query));
|
||
AddAuthorizationHeaders(
|
||
request,
|
||
path,
|
||
HttpMethod.Get,
|
||
query);
|
||
return await SendForJsonResult(request);
|
||
}
|
||
|
||
private async Task<byte[]> Download(
|
||
string path,
|
||
Dictionary<string, string> query)
|
||
{
|
||
ValidateSettings();
|
||
using var request = new HttpRequestMessage(
|
||
HttpMethod.Get,
|
||
BuildUrl(path, query));
|
||
AddAuthorizationHeaders(
|
||
request,
|
||
path,
|
||
HttpMethod.Get,
|
||
query);
|
||
using var response = await Send(request);
|
||
var bytes = await response.Content.ReadAsByteArrayAsync();
|
||
if (!response.IsSuccessStatusCode || LooksLikeJson(response, bytes))
|
||
{
|
||
ThrowProviderError(bytes, response.StatusCode);
|
||
}
|
||
|
||
if (bytes.Length == 0)
|
||
throw Oops.Bah("契约锁下载文件内容为空");
|
||
return bytes;
|
||
}
|
||
|
||
private async Task<JsonElement> SendForJsonResult(
|
||
HttpRequestMessage request)
|
||
{
|
||
using var response = await Send(request);
|
||
var responseText = await response.Content.ReadAsStringAsync();
|
||
if (string.IsNullOrWhiteSpace(responseText))
|
||
throw Oops.Bah("契约锁返回内容为空");
|
||
|
||
using var document = JsonDocument.Parse(responseText);
|
||
var root = document.RootElement;
|
||
var hasCode = root.TryGetProperty("code", out var codeElement);
|
||
var code = hasCode ? ReadCode(codeElement) : 0;
|
||
var hasResponseCode = TryGetString(
|
||
root,
|
||
"responseCode",
|
||
out var responseCode);
|
||
var responseCodeSucceeded = !hasResponseCode
|
||
|| responseCode == "0"
|
||
|| responseCode == "00000000";
|
||
if (!response.IsSuccessStatusCode
|
||
|| (hasCode && code != 0)
|
||
|| !responseCodeSucceeded)
|
||
{
|
||
var hasMessage = TryGetString(
|
||
root,
|
||
"message",
|
||
out var providerMessage)
|
||
|| TryGetString(
|
||
root,
|
||
"responseMessage",
|
||
out providerMessage);
|
||
var message = hasMessage
|
||
? providerMessage
|
||
: $"HTTP {(int)response.StatusCode},响应码 {responseCode ?? code.ToString()}";
|
||
var providerCode = hasResponseCode
|
||
? responseCode
|
||
: hasCode
|
||
? code.ToString(CultureInfo.InvariantCulture)
|
||
: ((int)response.StatusCode).ToString(CultureInfo.InvariantCulture);
|
||
|
||
throw Oops.Bah($"契约锁请求失败:{message}(响应码:{providerCode})");
|
||
}
|
||
|
||
if (!root.TryGetProperty("result", out var result))
|
||
return root.Clone();
|
||
return result.Clone();
|
||
}
|
||
|
||
private async Task<HttpResponseMessage> Send(
|
||
HttpRequestMessage request)
|
||
{
|
||
using var timeout = new CancellationTokenSource(
|
||
TimeSpan.FromSeconds(Math.Max(10, _setting.TimeoutSeconds)));
|
||
try
|
||
{
|
||
return await HttpClient.SendAsync(
|
||
request,
|
||
HttpCompletionOption.ResponseHeadersRead,
|
||
timeout.Token);
|
||
}
|
||
catch (OperationCanceledException)
|
||
{
|
||
throw Oops.Bah("契约锁请求超时,请稍后重试");
|
||
}
|
||
catch (HttpRequestException exception)
|
||
{
|
||
throw Oops.Bah($"无法连接契约锁:{exception.Message}");
|
||
}
|
||
}
|
||
|
||
private void AddAuthorizationHeaders(
|
||
HttpRequestMessage request,
|
||
string path,
|
||
HttpMethod method,
|
||
Dictionary<string, string> parameters = null,
|
||
byte[] fileBytes = null,
|
||
string jsonBody = null)
|
||
{
|
||
var timestamp = DateTimeOffset.UtcNow
|
||
.ToUnixTimeMilliseconds()
|
||
.ToString(CultureInfo.InvariantCulture);
|
||
var nonce = Guid.NewGuid().ToString();
|
||
var signature = string.Equals(
|
||
_setting.SignatureVersion,
|
||
SignatureHmacSha256,
|
||
StringComparison.OrdinalIgnoreCase)
|
||
? BuildHmacSha256Signature(
|
||
path,
|
||
method,
|
||
timestamp,
|
||
nonce,
|
||
parameters,
|
||
fileBytes,
|
||
jsonBody)
|
||
: BuildMd5Signature(timestamp, nonce);
|
||
|
||
request.Headers.TryAddWithoutValidation(
|
||
AccessTokenHeader,
|
||
_setting.AppToken.Trim());
|
||
request.Headers.TryAddWithoutValidation(TimestampHeader, timestamp);
|
||
request.Headers.TryAddWithoutValidation(NonceHeader, nonce);
|
||
request.Headers.TryAddWithoutValidation(SignatureHeader, signature);
|
||
request.Headers.TryAddWithoutValidation("version", "OnboardX-1.0");
|
||
if (!string.IsNullOrWhiteSpace(_setting.SignatureVersion))
|
||
{
|
||
request.Headers.TryAddWithoutValidation(
|
||
SignatureVersionHeader,
|
||
_setting.SignatureVersion.Trim());
|
||
}
|
||
}
|
||
|
||
private string BuildMd5Signature(string timestamp, string nonce)
|
||
{
|
||
var source = _setting.AppToken.Trim()
|
||
+ _setting.AppSecret.Trim()
|
||
+ timestamp
|
||
+ nonce;
|
||
return Convert.ToHexString(MD5.HashData(Encoding.UTF8.GetBytes(source)))
|
||
.ToLowerInvariant();
|
||
}
|
||
|
||
private string BuildHmacSha256Signature(
|
||
string path,
|
||
HttpMethod method,
|
||
string timestamp,
|
||
string nonce,
|
||
Dictionary<string, string> parameters,
|
||
byte[] fileBytes,
|
||
string jsonBody)
|
||
{
|
||
var values = new List<KeyValuePair<string, string>>
|
||
{
|
||
new(AccessTokenHeader, _setting.AppToken.Trim()),
|
||
new(TimestampHeader, timestamp),
|
||
new(NonceHeader, nonce),
|
||
new(SignatureVersionHeader, SignatureHmacSha256)
|
||
};
|
||
if (!string.IsNullOrWhiteSpace(jsonBody))
|
||
values.Add(new KeyValuePair<string, string>(
|
||
RequestBodySignatureKey,
|
||
jsonBody));
|
||
if (parameters != null)
|
||
{
|
||
values.AddRange(parameters.Where(item =>
|
||
!string.IsNullOrWhiteSpace(item.Key)
|
||
&& !string.IsNullOrWhiteSpace(item.Value)));
|
||
}
|
||
if (fileBytes != null)
|
||
{
|
||
values.Add(new KeyValuePair<string, string>(
|
||
"file",
|
||
Convert.ToHexString(MD5.HashData(fileBytes))
|
||
.ToLowerInvariant()));
|
||
}
|
||
|
||
var builder = new StringBuilder()
|
||
.Append("requestMethod=")
|
||
.Append(method.Method)
|
||
.Append("&requestUrl=")
|
||
.Append(path);
|
||
foreach (var value in values
|
||
.OrderBy(item => item.Key, StringComparer.Ordinal))
|
||
{
|
||
builder.Append('&')
|
||
.Append(value.Key.Trim())
|
||
.Append('=')
|
||
.Append(value.Value.Trim());
|
||
}
|
||
|
||
using var hmac = new HMACSHA256(
|
||
Encoding.UTF8.GetBytes(_setting.AppSecret.Trim()));
|
||
return Convert.ToHexString(
|
||
hmac.ComputeHash(Encoding.UTF8.GetBytes(builder.ToString())))
|
||
.ToLowerInvariant();
|
||
}
|
||
|
||
private string BuildUrl(
|
||
string path,
|
||
Dictionary<string, string> query = null)
|
||
{
|
||
var url = $"{_setting.ServerUrl.Trim().TrimEnd('/')}/{path.TrimStart('/')}";
|
||
if (query == null || query.Count == 0)
|
||
return url;
|
||
var queryString = string.Join("&", query.Select(item =>
|
||
Uri.EscapeDataString(item.Key)
|
||
+ "="
|
||
+ Uri.EscapeDataString(item.Value ?? string.Empty)));
|
||
return url + "?" + queryString;
|
||
}
|
||
|
||
private void ValidateSettings()
|
||
{
|
||
if (!_setting.Enabled)
|
||
throw Oops.Bah("契约锁电子签功能尚未启用");
|
||
if (string.IsNullOrWhiteSpace(_setting.ServerUrl))
|
||
throw Oops.Bah("请配置契约锁 ServerUrl");
|
||
if (string.IsNullOrWhiteSpace(_setting.AppToken))
|
||
throw Oops.Bah("请配置契约锁 AppToken");
|
||
if (string.IsNullOrWhiteSpace(_setting.AppSecret))
|
||
throw Oops.Bah("请配置契约锁 AppSecret");
|
||
}
|
||
|
||
private static int ReadCode(JsonElement element)
|
||
{
|
||
if (element.ValueKind == JsonValueKind.Number
|
||
&& element.TryGetInt32(out var numericCode))
|
||
{
|
||
return numericCode;
|
||
}
|
||
return int.TryParse(element.ToString(), out var textCode)
|
||
? textCode
|
||
: -1;
|
||
}
|
||
|
||
private static bool TryGetString(
|
||
JsonElement element,
|
||
string propertyName,
|
||
out string value)
|
||
{
|
||
value = null;
|
||
if (element.ValueKind != JsonValueKind.Object
|
||
|| !element.TryGetProperty(propertyName, out var property)
|
||
|| property.ValueKind == JsonValueKind.Null)
|
||
{
|
||
return false;
|
||
}
|
||
|
||
value = property.ValueKind == JsonValueKind.String
|
||
? property.GetString()
|
||
: property.ToString();
|
||
return !string.IsNullOrWhiteSpace(value);
|
||
}
|
||
|
||
private static bool LooksLikeJson(
|
||
HttpResponseMessage response,
|
||
byte[] bytes)
|
||
{
|
||
var contentType = response.Content.Headers.ContentType?.MediaType;
|
||
if (!string.IsNullOrWhiteSpace(contentType)
|
||
&& contentType.Contains("json", StringComparison.OrdinalIgnoreCase))
|
||
{
|
||
return true;
|
||
}
|
||
|
||
var first = bytes.FirstOrDefault(item =>
|
||
!char.IsWhiteSpace((char)item));
|
||
return first == '{' || first == '[';
|
||
}
|
||
|
||
private static void ThrowProviderError(
|
||
byte[] bytes,
|
||
global::System.Net.HttpStatusCode statusCode)
|
||
{
|
||
var text = Encoding.UTF8.GetString(bytes);
|
||
try
|
||
{
|
||
using var document = JsonDocument.Parse(text);
|
||
var root = document.RootElement;
|
||
var hasMessage = TryGetString(
|
||
root,
|
||
"message",
|
||
out var providerMessage)
|
||
|| TryGetString(
|
||
root,
|
||
"responseMessage",
|
||
out providerMessage);
|
||
var message = hasMessage ? providerMessage : text;
|
||
throw Oops.Bah($"契约锁下载失败:{message}");
|
||
}
|
||
catch (JsonException)
|
||
{
|
||
throw Oops.Bah(
|
||
$"契约锁下载失败:HTTP {(int)statusCode}");
|
||
}
|
||
}
|
||
|
||
private static string NullIfBlank(string value)
|
||
{
|
||
return string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||
}
|
||
|
||
/// <summary>
|
||
/// 构造兼容契约锁长整型 Id 和字符串 Id 两种返回形式的 JSON 配置。
|
||
/// </summary>
|
||
private static JsonSerializerOptions CreateJsonOptions()
|
||
{
|
||
var options = new JsonSerializerOptions
|
||
{
|
||
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
|
||
PropertyNameCaseInsensitive = true,
|
||
DefaultIgnoreCondition = JsonIgnoreCondition.WhenWritingNull
|
||
};
|
||
options.Converters.Add(new FlexibleStringJsonConverter());
|
||
return options;
|
||
}
|
||
|
||
/// <summary>
|
||
/// 将第三方响应中的字符串或数字统一读取为字符串,避免长整型 Id 精度丢失。
|
||
/// </summary>
|
||
private sealed class FlexibleStringJsonConverter : JsonConverter<string>
|
||
{
|
||
public override string Read(
|
||
ref Utf8JsonReader reader,
|
||
Type typeToConvert,
|
||
JsonSerializerOptions options)
|
||
{
|
||
return reader.TokenType switch
|
||
{
|
||
JsonTokenType.String => reader.GetString(),
|
||
JsonTokenType.Number => GetRawNumber(ref reader),
|
||
JsonTokenType.True => bool.TrueString,
|
||
JsonTokenType.False => bool.FalseString,
|
||
_ => reader.GetString()
|
||
};
|
||
}
|
||
|
||
public override void Write(
|
||
Utf8JsonWriter writer,
|
||
string value,
|
||
JsonSerializerOptions options)
|
||
{
|
||
writer.WriteStringValue(value);
|
||
}
|
||
|
||
private static string GetRawNumber(ref Utf8JsonReader reader)
|
||
{
|
||
if (reader.TryGetInt64(out var integer))
|
||
return integer.ToString(CultureInfo.InvariantCulture);
|
||
return reader.GetDecimal().ToString(CultureInfo.InvariantCulture);
|
||
}
|
||
}
|
||
}
|