From 0fdc9e29f42c083fba75d7681dce5fc800a250e9 Mon Sep 17 00:00:00 2001 From: zzh <1368735060@qq.com> Date: Sat, 12 Sep 2026 17:40:52 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20API=E4=B8=AD=E5=BF=83=E5=85=B6=E4=BB=96?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E4=B8=8E=E9=9D=A2=E5=8D=95=20COS=20=E6=B5=81?= =?UTF-8?q?=E4=BB=A3=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 将 itfx 网关优先读其他配置 api_center(含 AppKey/AppSecret 回退),面单预览经服务端代理拉取 COS。 Co-authored-by: Cursor --- .../Integrations/Itfx/ItfxForwardClient.cs | 7 +- .../Itfx/ItfxGatewayConfigProvider.cs | 57 ++++++ .../Integrations/Itfx/ItfxGatewayOptions.cs | 10 + .../Integrations/Itfx/ItfxMetadataClient.cs | 5 +- .../Itfx/ItfxServiceExtensions.cs | 1 + .../Services/Storage/FileStorageService.cs | 178 ++++++++++++++++-- .../System/SystemOtherConfigService.cs | 17 ++ .../OrderDownstreamCredentialResolver.cs | 78 +++++++- .../Storage/FileStorageServiceExtensions.cs | 7 + .../Consts/SystemOtherConfigCodes.cs | 5 + 10 files changed, 340 insertions(+), 25 deletions(-) create mode 100644 src/Oms.Application/Integrations/Itfx/ItfxGatewayConfigProvider.cs diff --git a/src/Oms.Application/Integrations/Itfx/ItfxForwardClient.cs b/src/Oms.Application/Integrations/Itfx/ItfxForwardClient.cs index 949bf88..4ab5564 100644 --- a/src/Oms.Application/Integrations/Itfx/ItfxForwardClient.cs +++ b/src/Oms.Application/Integrations/Itfx/ItfxForwardClient.cs @@ -3,13 +3,12 @@ using System.Net.Http.Json; using System.Text.Json; using Fake.DependencyInjection; using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.Options; namespace Oms.Application.Integrations.Itfx; internal sealed class ItfxForwardClient( IHttpClientFactory httpClientFactory, - IOptions options, + IItfxGatewayConfigProvider configProvider, IHttpContextAccessor httpContextAccessor) : IItfxForwardClient, IScopedDependency { public const string HttpClientName = nameof(ItfxForwardClient); @@ -45,10 +44,10 @@ internal sealed class ItfxForwardClient( object body, CancellationToken cancellationToken = default) { - var settings = options.Value; + var settings = await configProvider.GetAsync(cancellationToken); if (string.IsNullOrWhiteSpace(settings.BaseUrl)) { - throw new InvalidOperationException("Itfx BaseUrl not configured"); + throw new InvalidOperationException("API 中心服务地址未配置,请在「其他配置」中启用 api_center"); } if (string.IsNullOrWhiteSpace(standardCode)) diff --git a/src/Oms.Application/Integrations/Itfx/ItfxGatewayConfigProvider.cs b/src/Oms.Application/Integrations/Itfx/ItfxGatewayConfigProvider.cs new file mode 100644 index 0000000..aa0a937 --- /dev/null +++ b/src/Oms.Application/Integrations/Itfx/ItfxGatewayConfigProvider.cs @@ -0,0 +1,57 @@ +using Fake.DependencyInjection; +using Microsoft.Extensions.Options; +using Oms.Domain.SystemAggregate.Consts; +using Oms.Domain.SystemAggregate.Repositories; + +namespace Oms.Application.Integrations.Itfx; + +/// +/// itfx / API 中心运行时配置(优先「其他配置」api_center,回退 appsettings Oms:Itfx)。 +/// +public interface IItfxGatewayConfigProvider +{ + Task GetAsync(CancellationToken cancellationToken = default); +} + +internal sealed class ItfxGatewayConfigProvider( + ISystemOtherConfigRepository repository, + IOptions options) : IItfxGatewayConfigProvider, IScopedDependency +{ + public async Task GetAsync(CancellationToken cancellationToken = default) + { + var fallback = options.Value; + var entity = await repository.GetByConfigCodeAsync(SystemOtherConfigCodes.ApiCenter, cancellationToken); + if (entity == null || !entity.Enabled) + { + return Clone(fallback); + } + + return new ItfxGatewayOptions + { + BaseUrl = Coalesce(entity.BaseUrl, fallback.BaseUrl), + AppKey = Coalesce(entity.AppKey, fallback.AppKey), + AppSecret = Coalesce(entity.AppSecret, fallback.AppSecret), + TimeoutSeconds = entity.TimeoutSeconds > 0 ? entity.TimeoutSeconds : fallback.TimeoutSeconds, + Customs9610StandardCode = fallback.Customs9610StandardCode, + Customs9610ManifestStandardCode = fallback.Customs9610ManifestStandardCode, + Customs9610DeclarationFormStandardCode = fallback.Customs9610DeclarationFormStandardCode, + Customs9610SupplierCode = fallback.Customs9610SupplierCode + }; + } + + private static ItfxGatewayOptions Clone(ItfxGatewayOptions src) => + new() + { + BaseUrl = src.BaseUrl, + AppKey = src.AppKey, + AppSecret = src.AppSecret, + TimeoutSeconds = src.TimeoutSeconds, + Customs9610StandardCode = src.Customs9610StandardCode, + Customs9610ManifestStandardCode = src.Customs9610ManifestStandardCode, + Customs9610DeclarationFormStandardCode = src.Customs9610DeclarationFormStandardCode, + Customs9610SupplierCode = src.Customs9610SupplierCode + }; + + private static string Coalesce(string? primary, string fallback) => + string.IsNullOrWhiteSpace(primary) ? fallback : primary.Trim(); +} diff --git a/src/Oms.Application/Integrations/Itfx/ItfxGatewayOptions.cs b/src/Oms.Application/Integrations/Itfx/ItfxGatewayOptions.cs index 6cbe9af..c9b0005 100644 --- a/src/Oms.Application/Integrations/Itfx/ItfxGatewayOptions.cs +++ b/src/Oms.Application/Integrations/Itfx/ItfxGatewayOptions.cs @@ -12,6 +12,16 @@ public class ItfxGatewayOptions /// public string BaseUrl { get; set; } = "https://api.ideship.com/itfx"; + /// + /// 默认应用 AppKey(其他配置 api_center;GetInfo 无 Secret 时回退)。 + /// + public string AppKey { get; set; } = string.Empty; + + /// + /// 默认应用 AppSecret(其他配置 api_center;GetInfo 无 Secret 时回退)。 + /// + public string AppSecret { get; set; } = string.Empty; + public int TimeoutSeconds { get; set; } = 30; /// diff --git a/src/Oms.Application/Integrations/Itfx/ItfxMetadataClient.cs b/src/Oms.Application/Integrations/Itfx/ItfxMetadataClient.cs index 9d4969b..006ca70 100644 --- a/src/Oms.Application/Integrations/Itfx/ItfxMetadataClient.cs +++ b/src/Oms.Application/Integrations/Itfx/ItfxMetadataClient.cs @@ -3,13 +3,12 @@ using System.Text.Json; using Fake.DependencyInjection; using Microsoft.AspNetCore.Http; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; namespace Oms.Application.Integrations.Itfx; internal sealed class ItfxMetadataClient( IHttpClientFactory httpClientFactory, - IOptions options, + IItfxGatewayConfigProvider configProvider, IHttpContextAccessor httpContextAccessor, ILogger logger) : IItfxMetadataClient, IScopedDependency { @@ -67,7 +66,7 @@ internal sealed class ItfxMetadataClient( CancellationToken cancellationToken) where T : class { - var settings = options.Value; + var settings = await configProvider.GetAsync(cancellationToken); if (string.IsNullOrWhiteSpace(settings.BaseUrl)) { logger.LogDebug("Itfx BaseUrl not configured, skip {Url}", relativeUrl); diff --git a/src/Oms.Application/Integrations/Itfx/ItfxServiceExtensions.cs b/src/Oms.Application/Integrations/Itfx/ItfxServiceExtensions.cs index ffe6558..9da8a55 100644 --- a/src/Oms.Application/Integrations/Itfx/ItfxServiceExtensions.cs +++ b/src/Oms.Application/Integrations/Itfx/ItfxServiceExtensions.cs @@ -10,6 +10,7 @@ public static class ItfxServiceExtensions services.Configure(configuration.GetSection(ItfxGatewayOptions.SectionName)); services.AddHttpClient(ItfxMetadataClient.HttpClientName); services.AddHttpClient(ItfxForwardClient.HttpClientName); + services.AddScoped(); services.AddScoped(); services.AddScoped(); return services; diff --git a/src/Oms.Application/Services/Storage/FileStorageService.cs b/src/Oms.Application/Services/Storage/FileStorageService.cs index 016b3b2..9223b0e 100644 --- a/src/Oms.Application/Services/Storage/FileStorageService.cs +++ b/src/Oms.Application/Services/Storage/FileStorageService.cs @@ -1,6 +1,8 @@ +using System.Net.Http.Headers; using Fake.Application; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; using Oms.Application.Storage; namespace Oms.Application.Services.Storage; @@ -12,7 +14,12 @@ namespace Oms.Application.Services.Storage; [ApiExplorerSettings(GroupName = "OMS-Storage")] public class FileStorageService : ApplicationService { + public const string PrintProxyHttpClientName = "FileStoragePrintProxy"; + private IFileStorage FileStorage => LazyServiceProvider.GetRequiredService(); + private IHttpClientFactory HttpClientFactory => LazyServiceProvider.GetRequiredService(); + private ILogger FileLogger => + LazyServiceProvider.GetRequiredService>(); /// /// 是否支持前端直传(Cos=true)。 @@ -111,6 +118,7 @@ public class FileStorageService : ApplicationService /// /// 按 key / URL 流式读取文件(供面单预览等场景,避免前端跨域 iframe 空白)。 + /// 若对象键解析失败,但对受管 COS 的预签名绝对 URL 仍可服务端代拉(绕开浏览器 CORS)。 /// [HttpGet("/file-storage/stream")] public async Task StreamAsync([FromQuery] string key, CancellationToken cancellationToken = default) @@ -120,43 +128,189 @@ public class FileStorageService : ApplicationService throw new BusinessException("key 不能为空"); } - string storageKey; + var raw = key.Trim(); + Exception? storageError = null; + try { - storageKey = FileStorage.ToStorageKey(key) ?? FileStoragePath.NormalizeKey(key); + var storageKey = FileStorage.ToStorageKey(raw) ?? FileStoragePath.NormalizeKey(raw); + var stream = await FileStorage.OpenReadAsync(storageKey, cancellationToken); + return new FileStreamResult(stream, GuessStreamContentType(storageKey)) + { + EnableRangeProcessing = true, + }; } - catch (Exception) + catch (Exception ex) { - throw new BusinessException("无法解析文件 key"); + storageError = ex; + FileLogger.LogWarning(ex, "file-storage/stream 按 key 读取失败,尝试签名 URL 代拉: {Key}", TruncateForLog(raw)); } - if (!await FileStorage.ExistsAsync(storageKey, cancellationToken)) + if (IsAllowedPrintProxyUrl(raw)) { - throw new BusinessException("文件不存在或已过期"); + try + { + return await ProxySignedUrlAsync(raw, cancellationToken); + } + catch (Exception ex) + { + FileLogger.LogWarning(ex, "file-storage/stream 签名 URL 代拉失败: {Key}", TruncateForLog(raw)); + storageError ??= ex; + } } - var stream = await FileStorage.OpenReadAsync(storageKey, cancellationToken); - return new FileStreamResult(stream, GuessStreamContentType(storageKey)) + if (storageError is BusinessException business) { - EnableRangeProcessing = true, + throw business; + } + + throw new BusinessException("文件不存在或已过期"); + } + + private async Task ProxySignedUrlAsync(string absoluteUrl, CancellationToken cancellationToken) + { + var client = HttpClientFactory.CreateClient(PrintProxyHttpClientName); + using var request = new HttpRequestMessage(HttpMethod.Get, absoluteUrl); + request.Headers.Accept.Add(new MediaTypeWithQualityHeaderValue("*/*")); + + var response = await client.SendAsync( + request, + HttpCompletionOption.ResponseHeadersRead, + cancellationToken); + + if (!response.IsSuccessStatusCode) + { + FileLogger.LogWarning( + "签名 URL 代拉 HTTP {Status}: {Url}", + (int)response.StatusCode, + TruncateForLog(absoluteUrl)); + throw new BusinessException($"面单代拉失败 HTTP {(int)response.StatusCode}"); + } + + var contentType = response.Content.Headers.ContentType?.ToString(); + if (string.IsNullOrWhiteSpace(contentType)) + { + contentType = GuessStreamContentType(absoluteUrl); + } + + var stream = await response.Content.ReadAsStreamAsync(cancellationToken); + // 由 FileStreamResult 在响应结束时释放;HttpResponseMessage 需保持至流读完 + return new FileStreamResult(new HttpResponseStream(response, stream), contentType) + { + EnableRangeProcessing = false, }; } + private static bool IsAllowedPrintProxyUrl(string url) + { + if (!Uri.TryCreate(url, UriKind.Absolute, out var uri)) + { + return false; + } + + if (!string.Equals(uri.Scheme, Uri.UriSchemeHttps, StringComparison.OrdinalIgnoreCase)) + { + return false; + } + + var host = uri.Host.ToLowerInvariant(); + return host is "cos.ideship.com" or "cos.nanafa.com" + || host.EndsWith(".myqcloud.com", StringComparison.Ordinal) + || host.StartsWith("cos.", StringComparison.Ordinal); + } + + private static string TruncateForLog(string value) => + value.Length <= 160 ? value : value[..160] + "…"; + private static string GuessStreamContentType(string storageKey) { - if (storageKey.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase)) + var path = storageKey; + if (FileStoragePath.IsAbsoluteUrl(storageKey) + && Uri.TryCreate(storageKey, UriKind.Absolute, out var uri)) + { + path = uri.AbsolutePath; + } + + if (path.EndsWith(".pdf", StringComparison.OrdinalIgnoreCase) + || path.Contains(".pdf?", StringComparison.OrdinalIgnoreCase)) { return "application/pdf"; } - if (storageKey.EndsWith(".html", StringComparison.OrdinalIgnoreCase) - || storageKey.EndsWith(".htm", StringComparison.OrdinalIgnoreCase)) + if (path.EndsWith(".html", StringComparison.OrdinalIgnoreCase) + || path.EndsWith(".htm", StringComparison.OrdinalIgnoreCase) + || path.Contains(".html?", StringComparison.OrdinalIgnoreCase) + || path.Contains(".htm?", StringComparison.OrdinalIgnoreCase)) { return "text/html; charset=utf-8"; } return "application/octet-stream"; } + + /// + /// 包装 HttpResponseMessage + 响应流,确保 FileStreamResult 读完后释放连接。 + /// + private sealed class HttpResponseStream : Stream + { + private readonly HttpResponseMessage _response; + private readonly Stream _inner; + + public HttpResponseStream(HttpResponseMessage response, Stream inner) + { + _response = response; + _inner = inner; + } + + public override bool CanRead => _inner.CanRead; + public override bool CanSeek => false; + public override bool CanWrite => false; + public override long Length => throw new NotSupportedException(); + + public override long Position + { + get => throw new NotSupportedException(); + set => throw new NotSupportedException(); + } + + public override void Flush() => _inner.Flush(); + + public override int Read(byte[] buffer, int offset, int count) => + _inner.Read(buffer, offset, count); + + public override Task ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) => + _inner.ReadAsync(buffer, offset, count, cancellationToken); + + public override ValueTask ReadAsync( + Memory buffer, + CancellationToken cancellationToken = default) => + _inner.ReadAsync(buffer, cancellationToken); + + public override long Seek(long offset, SeekOrigin origin) => throw new NotSupportedException(); + + public override void SetLength(long value) => throw new NotSupportedException(); + + public override void Write(byte[] buffer, int offset, int count) => + throw new NotSupportedException(); + + public override async ValueTask DisposeAsync() + { + await _inner.DisposeAsync(); + _response.Dispose(); + await base.DisposeAsync(); + } + + protected override void Dispose(bool disposing) + { + if (disposing) + { + _inner.Dispose(); + _response.Dispose(); + } + + base.Dispose(disposing); + } + } } public class PresignUploadInput diff --git a/src/Oms.Application/Services/System/SystemOtherConfigService.cs b/src/Oms.Application/Services/System/SystemOtherConfigService.cs index b2820b1..2747909 100644 --- a/src/Oms.Application/Services/System/SystemOtherConfigService.cs +++ b/src/Oms.Application/Services/System/SystemOtherConfigService.cs @@ -171,6 +171,23 @@ public class SystemOtherConfigService : ApplicationService throw new BusinessException("轨迹服务 AppSecret 不能为空"); } } + else if (string.Equals(configCode, SystemOtherConfigCodes.ApiCenter, StringComparison.OrdinalIgnoreCase)) + { + if (string.IsNullOrWhiteSpace(input.BaseUrl)) + { + throw new BusinessException("API 中心服务地址不能为空"); + } + + if (string.IsNullOrWhiteSpace(input.AppKey)) + { + throw new BusinessException("API 中心应用 AppKey 不能为空"); + } + + if (excludeId == null && ShouldKeepExistingSecret(input.AppSecret)) + { + throw new BusinessException("API 中心应用 AppSecret 不能为空"); + } + } else if (string.Equals(configCode, SystemOtherConfigCodes.WeChatMp, StringComparison.OrdinalIgnoreCase)) { if (string.IsNullOrWhiteSpace(input.AppKey)) diff --git a/src/Oms.Application/Shared/OrderDownstreamCredentialResolver.cs b/src/Oms.Application/Shared/OrderDownstreamCredentialResolver.cs index 9b21417..e3c52fb 100644 --- a/src/Oms.Application/Shared/OrderDownstreamCredentialResolver.cs +++ b/src/Oms.Application/Shared/OrderDownstreamCredentialResolver.cs @@ -22,7 +22,8 @@ public sealed class OrderDownstreamCredentials public class OrderDownstreamCredentialResolver( IServiceChannelRepository serviceChannelRepository, IServiceChannelGroupRepository serviceChannelGroupRepository, - IItfxMetadataClient itfxMetadataClient) : IScopedDependency + IItfxMetadataClient itfxMetadataClient, + IItfxGatewayConfigProvider itfxGatewayConfigProvider) : IScopedDependency { public async Task ResolveAsync(Order order, CancellationToken cancellationToken = default) { @@ -40,7 +41,19 @@ public class OrderDownstreamCredentialResolver( var appSecret = app?.AppSecret?.Trim(); if (string.IsNullOrWhiteSpace(appSecret)) { - throw new BusinessException("应用 AppSecret 未配置"); + appSecret = await TryResolveAppSecretFromApiCenterAsync(order.DownstreamAppKey, cancellationToken); + } + + if (string.IsNullOrWhiteSpace(appSecret)) + { + if (app == null) + { + throw new BusinessException( + "无法从 API 中心获取应用信息,请检查「其他配置-API中心服务」地址/AppKey/AppSecret,以及下游应用是否存在"); + } + + throw new BusinessException( + "应用 AppSecret 未配置,请在「其他配置-API中心服务」填写与下游一致的 AppKey/AppSecret"); } return new OrderDownstreamCredentials @@ -84,9 +97,28 @@ public class OrderDownstreamCredentialResolver( } var appInfo = await itfxMetadataClient.GetAppAsync(group.AppId, cancellationToken); - if (appInfo == null || string.IsNullOrWhiteSpace(appInfo.AppKey) || string.IsNullOrWhiteSpace(appInfo.AppSecret)) + var resolvedAppKey = appInfo?.AppKey?.Trim(); + var resolvedAppSecret = appInfo?.AppSecret?.Trim(); + if (string.IsNullOrWhiteSpace(resolvedAppKey) || string.IsNullOrWhiteSpace(resolvedAppSecret)) { - throw new BusinessException("应用 AppKey / AppSecret 未配置"); + var fromCenter = await TryResolveCredentialsFromApiCenterAsync(cancellationToken); + if (fromCenter != null) + { + resolvedAppKey = string.IsNullOrWhiteSpace(resolvedAppKey) ? fromCenter.AppKey : resolvedAppKey; + resolvedAppSecret = string.IsNullOrWhiteSpace(resolvedAppSecret) ? fromCenter.AppSecret : resolvedAppSecret; + } + } + + if (string.IsNullOrWhiteSpace(resolvedAppKey) || string.IsNullOrWhiteSpace(resolvedAppSecret)) + { + if (appInfo == null) + { + throw new BusinessException( + "无法从 API 中心获取应用信息,请检查「其他配置-API中心服务」地址/AppKey/AppSecret,以及服务商应用是否存在"); + } + + throw new BusinessException( + "应用 AppKey / AppSecret 未配置,请在「其他配置-API中心服务」填写应用凭证"); } var sp = await itfxMetadataClient.GetServiceProviderAsync(group.ApiServiceProviderId, cancellationToken); @@ -98,8 +130,42 @@ public class OrderDownstreamCredentialResolver( return new OrderDownstreamCredentials { SupplierCode = sp.Code.Trim(), - AppKey = appInfo.AppKey.Trim(), - AppSecret = appInfo.AppSecret.Trim() + AppKey = resolvedAppKey, + AppSecret = resolvedAppSecret + }; + } + + private async Task TryResolveAppSecretFromApiCenterAsync( + string expectedAppKey, + CancellationToken cancellationToken) + { + var cfg = await itfxGatewayConfigProvider.GetAsync(cancellationToken); + if (string.IsNullOrWhiteSpace(cfg.AppKey) || string.IsNullOrWhiteSpace(cfg.AppSecret)) + { + return null; + } + + if (!string.Equals(cfg.AppKey.Trim(), expectedAppKey.Trim(), StringComparison.OrdinalIgnoreCase)) + { + return null; + } + + return cfg.AppSecret.Trim(); + } + + private async Task TryResolveCredentialsFromApiCenterAsync( + CancellationToken cancellationToken) + { + var cfg = await itfxGatewayConfigProvider.GetAsync(cancellationToken); + if (string.IsNullOrWhiteSpace(cfg.AppKey) || string.IsNullOrWhiteSpace(cfg.AppSecret)) + { + return null; + } + + return new OrderDownstreamCredentials + { + AppKey = cfg.AppKey.Trim(), + AppSecret = cfg.AppSecret.Trim() }; } diff --git a/src/Oms.Application/Storage/FileStorageServiceExtensions.cs b/src/Oms.Application/Storage/FileStorageServiceExtensions.cs index fb38e7f..e4b1580 100644 --- a/src/Oms.Application/Storage/FileStorageServiceExtensions.cs +++ b/src/Oms.Application/Storage/FileStorageServiceExtensions.cs @@ -3,6 +3,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; +using Oms.Application.Services.Storage; namespace Oms.Application.Storage; @@ -25,6 +26,12 @@ public static class FileStorageServiceExtensions services.AddSingleton(); } + // 面单打印:服务端代拉 COS 预签名 URL(浏览器无 CORS) + services.AddHttpClient(FileStorageService.PrintProxyHttpClientName, client => + { + client.Timeout = TimeSpan.FromMinutes(2); + }); + services.AddHostedService(); return services; } diff --git a/src/Oms.Domain/SystemAggregate/Consts/SystemOtherConfigCodes.cs b/src/Oms.Domain/SystemAggregate/Consts/SystemOtherConfigCodes.cs index cb26867..b11706e 100644 --- a/src/Oms.Domain/SystemAggregate/Consts/SystemOtherConfigCodes.cs +++ b/src/Oms.Domain/SystemAggregate/Consts/SystemOtherConfigCodes.cs @@ -49,4 +49,9 @@ public static class SystemOtherConfigCodes /// 全局默认面单模板(AppKey=模板 Id,Enabled 控制是否生效)。 /// public const string LabelDefaultTemplate = "label_default_template"; + +/// +/// API 中心(接口通 / itfx)网关:BaseUrl=itfx 根地址,AppKey/AppSecret=默认应用凭证,TimeoutSeconds=超时。 +/// +public const string ApiCenter = "api_center"; }