feat: API中心其他配置与面单 COS 流代理
All checks were successful
Build and Deploy / deploy (push) Successful in 1m52s
All checks were successful
Build and Deploy / deploy (push) Successful in 1m52s
将 itfx 网关优先读其他配置 api_center(含 AppKey/AppSecret 回退),面单预览经服务端代理拉取 COS。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
parent
87abcecb65
commit
0fdc9e29f4
@ -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<ItfxGatewayOptions> 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))
|
||||
|
||||
@ -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;
|
||||
|
||||
/// <summary>
|
||||
/// itfx / API 中心运行时配置(优先「其他配置」api_center,回退 appsettings Oms:Itfx)。
|
||||
/// </summary>
|
||||
public interface IItfxGatewayConfigProvider
|
||||
{
|
||||
Task<ItfxGatewayOptions> GetAsync(CancellationToken cancellationToken = default);
|
||||
}
|
||||
|
||||
internal sealed class ItfxGatewayConfigProvider(
|
||||
ISystemOtherConfigRepository repository,
|
||||
IOptions<ItfxGatewayOptions> options) : IItfxGatewayConfigProvider, IScopedDependency
|
||||
{
|
||||
public async Task<ItfxGatewayOptions> 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();
|
||||
}
|
||||
@ -12,6 +12,16 @@ public class ItfxGatewayOptions
|
||||
/// </summary>
|
||||
public string BaseUrl { get; set; } = "https://api.ideship.com/itfx";
|
||||
|
||||
/// <summary>
|
||||
/// 默认应用 AppKey(其他配置 api_center;GetInfo 无 Secret 时回退)。
|
||||
/// </summary>
|
||||
public string AppKey { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// 默认应用 AppSecret(其他配置 api_center;GetInfo 无 Secret 时回退)。
|
||||
/// </summary>
|
||||
public string AppSecret { get; set; } = string.Empty;
|
||||
|
||||
public int TimeoutSeconds { get; set; } = 30;
|
||||
|
||||
/// <summary>
|
||||
|
||||
@ -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<ItfxGatewayOptions> options,
|
||||
IItfxGatewayConfigProvider configProvider,
|
||||
IHttpContextAccessor httpContextAccessor,
|
||||
ILogger<ItfxMetadataClient> 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);
|
||||
|
||||
@ -10,6 +10,7 @@ public static class ItfxServiceExtensions
|
||||
services.Configure<ItfxGatewayOptions>(configuration.GetSection(ItfxGatewayOptions.SectionName));
|
||||
services.AddHttpClient(ItfxMetadataClient.HttpClientName);
|
||||
services.AddHttpClient(ItfxForwardClient.HttpClientName);
|
||||
services.AddScoped<IItfxGatewayConfigProvider, ItfxGatewayConfigProvider>();
|
||||
services.AddScoped<IItfxMetadataClient, ItfxMetadataClient>();
|
||||
services.AddScoped<IItfxForwardClient, ItfxForwardClient>();
|
||||
return services;
|
||||
|
||||
@ -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<IFileStorage>();
|
||||
private IHttpClientFactory HttpClientFactory => LazyServiceProvider.GetRequiredService<IHttpClientFactory>();
|
||||
private ILogger<FileStorageService> FileLogger =>
|
||||
LazyServiceProvider.GetRequiredService<ILogger<FileStorageService>>();
|
||||
|
||||
/// <summary>
|
||||
/// 是否支持前端直传(Cos=true)。
|
||||
@ -111,6 +118,7 @@ public class FileStorageService : ApplicationService
|
||||
|
||||
/// <summary>
|
||||
/// 按 key / URL 流式读取文件(供面单预览等场景,避免前端跨域 iframe 空白)。
|
||||
/// 若对象键解析失败,但对受管 COS 的预签名绝对 URL 仍可服务端代拉(绕开浏览器 CORS)。
|
||||
/// </summary>
|
||||
[HttpGet("/file-storage/stream")]
|
||||
public async Task<IActionResult> 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<IActionResult> 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";
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 包装 HttpResponseMessage + 响应流,确保 FileStreamResult 读完后释放连接。
|
||||
/// </summary>
|
||||
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<int> ReadAsync(byte[] buffer, int offset, int count, CancellationToken cancellationToken) =>
|
||||
_inner.ReadAsync(buffer, offset, count, cancellationToken);
|
||||
|
||||
public override ValueTask<int> ReadAsync(
|
||||
Memory<byte> 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
|
||||
|
||||
@ -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))
|
||||
|
||||
@ -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<OrderDownstreamCredentials> 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<string?> 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<OrderDownstreamCredentials?> 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()
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -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<IFileStorage, LocalFileStorage>();
|
||||
}
|
||||
|
||||
// 面单打印:服务端代拉 COS 预签名 URL(浏览器无 CORS)
|
||||
services.AddHttpClient(FileStorageService.PrintProxyHttpClientName, client =>
|
||||
{
|
||||
client.Timeout = TimeSpan.FromMinutes(2);
|
||||
});
|
||||
|
||||
services.AddHostedService<FileStorageStartupLogger>();
|
||||
return services;
|
||||
}
|
||||
|
||||
@ -49,4 +49,9 @@ public static class SystemOtherConfigCodes
|
||||
/// 全局默认面单模板(AppKey=模板 Id,Enabled 控制是否生效)。
|
||||
/// </summary>
|
||||
public const string LabelDefaultTemplate = "label_default_template";
|
||||
|
||||
/// <summary>
|
||||
/// API 中心(接口通 / itfx)网关:BaseUrl=itfx 根地址,AppKey/AppSecret=默认应用凭证,TimeoutSeconds=超时。
|
||||
/// </summary>
|
||||
public const string ApiCenter = "api_center";
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user