From c2c03f63e332c1e76ad6ed2ecb8a59eb91b3ab2d Mon Sep 17 00:00:00 2001 From: zzh <1368735060@qq.com> Date: Thu, 16 Jul 2026 11:03:02 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E5=BA=97=E9=93=BA=E5=B9=B3=E5=8F=B0=20?= =?UTF-8?q?OAuth=20=E9=85=8D=E7=BD=AE=E8=BF=81=E5=85=A5=E5=85=B6=E4=BB=96?= =?UTF-8?q?=E9=85=8D=E7=BD=AE=E9=A1=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 运行时优先读 t_system_other_config,修正管理端客户提示文案,并提供配置种子 SQL。 Co-authored-by: Cursor --- sql/seed_platform_shop_other_config.sql | 47 ++++++ .../PlatformShop/PlatformShopApiClient.cs | 22 ++- .../PlatformShopConfigProvider.cs | 147 ++++++++++++++++++ .../PlatformShopServiceExtensions.cs | 1 + .../Services/Orders/PlatformStoreService.cs | 35 +++-- .../System/SystemOtherConfigService.cs | 36 +++++ .../Shared/WarehouseCustomerHelper.cs | 2 +- .../Consts/SystemOtherConfigCodes.cs | 25 +++ 8 files changed, 294 insertions(+), 21 deletions(-) create mode 100644 sql/seed_platform_shop_other_config.sql create mode 100644 src/Oms.Application/Integrations/PlatformShop/PlatformShopConfigProvider.cs diff --git a/sql/seed_platform_shop_other_config.sql b/sql/seed_platform_shop_other_config.sql new file mode 100644 index 0000000..6da4d51 --- /dev/null +++ b/sql/seed_platform_shop_other_config.sql @@ -0,0 +1,47 @@ +-- 店铺平台 OAuth 配置种子(其他配置页可改;已存在则跳过) +INSERT INTO t_system_other_config + (ConfigCode, ConfigName, Enabled, BaseUrl, AppKey, AppSecret, TimeoutSeconds, Remark, IsDeleted, CreateTime) +SELECT 'platform_shop', '店铺授权-公共', 1, 'http://localhost:5173', NULL, NULL, 30, NULL, 0, NOW() +WHERE NOT EXISTS (SELECT 1 FROM t_system_other_config WHERE ConfigCode = 'platform_shop' AND IsDeleted = 0); + +INSERT INTO t_system_other_config + (ConfigCode, ConfigName, Enabled, BaseUrl, AppKey, AppSecret, TimeoutSeconds, Remark, IsDeleted, CreateTime) +SELECT 'platform_shop_tiktok', '店铺授权-TikTok', 1, + 'https://auth.tiktok-shops.com', + '6hc64lc6vl312', + '5a691a96ee6d844534769bab5c21de1f848fb678', + 30, + 'https://open-api.tiktokglobalshop.com', + 0, NOW() +WHERE NOT EXISTS (SELECT 1 FROM t_system_other_config WHERE ConfigCode = 'platform_shop_tiktok' AND IsDeleted = 0); + +INSERT INTO t_system_other_config + (ConfigCode, ConfigName, Enabled, BaseUrl, AppKey, AppSecret, TimeoutSeconds, Remark, IsDeleted, CreateTime) +SELECT 'platform_shop_temu', '店铺授权-Temu', 1, + 'https://openapi-b-us.temu.com/openapi/router', + '4ebbc9190ae410443d65b4c2faca981f', + '4782d2d827276688bf4758bed55dbdd4bbe79a79', + 30, NULL, 0, NOW() +WHERE NOT EXISTS (SELECT 1 FROM t_system_other_config WHERE ConfigCode = 'platform_shop_temu' AND IsDeleted = 0); + +INSERT INTO t_system_other_config + (ConfigCode, ConfigName, Enabled, BaseUrl, AppKey, AppSecret, TimeoutSeconds, Remark, IsDeleted, CreateTime) +SELECT 'platform_shop_shopee', '店铺授权-Shopee', 1, + 'https://partner.shopeemobile.com', + '2004112', + '2f385d7ea38b087d5d71f7292e06a09c9f19022ca6c3f60ec0110a8214ef7d65', + 30, + 'https://openplatform.shopee.cn', + 0, NOW() +WHERE NOT EXISTS (SELECT 1 FROM t_system_other_config WHERE ConfigCode = 'platform_shop_shopee' AND IsDeleted = 0); + +INSERT INTO t_system_other_config + (ConfigCode, ConfigName, Enabled, BaseUrl, AppKey, AppSecret, TimeoutSeconds, Remark, IsDeleted, CreateTime) +SELECT 'platform_shop_lazada', '店铺授权-Lazada', 1, + 'https://auth.lazada.com/oauth/authorize', + '123631', + '7vBz2EaD6rIupeGEfwrgkabsTNsXt2ix', + 30, + 'https://auth.lazada.com/rest', + 0, NOW() +WHERE NOT EXISTS (SELECT 1 FROM t_system_other_config WHERE ConfigCode = 'platform_shop_lazada' AND IsDeleted = 0); diff --git a/src/Oms.Application/Integrations/PlatformShop/PlatformShopApiClient.cs b/src/Oms.Application/Integrations/PlatformShop/PlatformShopApiClient.cs index f0dc06d..0c5582e 100644 --- a/src/Oms.Application/Integrations/PlatformShop/PlatformShopApiClient.cs +++ b/src/Oms.Application/Integrations/PlatformShop/PlatformShopApiClient.cs @@ -2,13 +2,12 @@ using System.Net.Http.Json; using System.Text; using System.Text.Json; using Microsoft.Extensions.Logging; -using Microsoft.Extensions.Options; namespace Oms.Application.Integrations.PlatformShop; public interface IPlatformShopApiClient { - string BuildAuthUrl(string platformCode, string redirectUri, string state, string? siteCountry); + Task BuildAuthUrlAsync(string platformCode, string redirectUri, string state, string? siteCountry, CancellationToken ct = default); Task ExchangeTokenAsync(PlatformShopTokenExchangeRequest request, CancellationToken ct = default); Task CreateTemuStoreTokenAsync(string authorizationCode, CancellationToken ct = default); } @@ -45,15 +44,26 @@ public sealed class PlatformShopBoundShop internal sealed class PlatformShopApiClient( IHttpClientFactory httpClientFactory, - IOptions options, + IPlatformShopConfigProvider configProvider, ILogger logger) : IPlatformShopApiClient { public const string HttpClientName = nameof(PlatformShopApiClient); - private readonly PlatformShopOptions _options = options.Value; + private PlatformShopOptions _options = new(); - public string BuildAuthUrl(string platformCode, string redirectUri, string state, string? siteCountry) + private async Task LoadOptionsAsync(CancellationToken ct = default) { + _options = await configProvider.GetAsync(ct); + } + + public async Task BuildAuthUrlAsync( + string platformCode, + string redirectUri, + string state, + string? siteCountry, + CancellationToken ct = default) + { + await LoadOptionsAsync(ct); return platformCode.ToLowerInvariant() switch { "lazada" => BuildLazadaAuthUrl(redirectUri, siteCountry), @@ -67,6 +77,7 @@ internal sealed class PlatformShopApiClient( PlatformShopTokenExchangeRequest request, CancellationToken ct = default) { + await LoadOptionsAsync(ct); return request.PlatformCode.ToLowerInvariant() switch { "lazada" => await ExchangeLazadaAsync(request.Code, ct), @@ -78,6 +89,7 @@ internal sealed class PlatformShopApiClient( public async Task CreateTemuStoreTokenAsync(string authorizationCode, CancellationToken ct = default) { + await LoadOptionsAsync(ct); EnsureTemuConfigured(); var temu = _options.Temu; var body = new Dictionary diff --git a/src/Oms.Application/Integrations/PlatformShop/PlatformShopConfigProvider.cs b/src/Oms.Application/Integrations/PlatformShop/PlatformShopConfigProvider.cs new file mode 100644 index 0000000..40cfb3a --- /dev/null +++ b/src/Oms.Application/Integrations/PlatformShop/PlatformShopConfigProvider.cs @@ -0,0 +1,147 @@ +using Fake.DependencyInjection; +using Microsoft.Extensions.Options; +using Oms.Domain.SystemAggregate.Consts; +using Oms.Domain.SystemAggregate.Entities; +using Oms.Domain.SystemAggregate.Repositories; + +namespace Oms.Application.Integrations.PlatformShop; + +/// +/// 平台店铺 OAuth 运行时配置:优先读「其他配置」,回退 appsettings。 +/// +public interface IPlatformShopConfigProvider +{ + Task GetAsync(CancellationToken cancellationToken = default); +} + +internal sealed class PlatformShopConfigProvider( + ISystemOtherConfigRepository repository, + IOptions options) : IPlatformShopConfigProvider, IScopedDependency +{ + public async Task GetAsync(CancellationToken cancellationToken = default) + { + var fallback = options.Value ?? new PlatformShopOptions(); + var common = await repository.GetByConfigCodeAsync(SystemOtherConfigCodes.PlatformShop, cancellationToken); + var lazada = await repository.GetByConfigCodeAsync(SystemOtherConfigCodes.PlatformShopLazada, cancellationToken); + var shopee = await repository.GetByConfigCodeAsync(SystemOtherConfigCodes.PlatformShopShopee, cancellationToken); + var tiktok = await repository.GetByConfigCodeAsync(SystemOtherConfigCodes.PlatformShopTiktok, cancellationToken); + var temu = await repository.GetByConfigCodeAsync(SystemOtherConfigCodes.PlatformShopTemu, cancellationToken); + + return new PlatformShopOptions + { + FrontendBaseUrl = Coalesce(common?.BaseUrl, fallback.FrontendBaseUrl), + StateSecret = Coalesce(common?.Remark, fallback.StateSecret), + Lazada = MergeLazada(lazada, fallback.Lazada), + Shopee = MergeShopee(shopee, fallback.Shopee), + Tiktok = MergeTiktok(tiktok, fallback.Tiktok), + Temu = MergeTemu(temu, fallback.Temu) + }; + } + + private static LazadaPlatformOptions MergeLazada(SystemOtherConfig? entity, LazadaPlatformOptions fallback) + { + fallback ??= new LazadaPlatformOptions(); + if (entity == null || !entity.Enabled) + { + return CloneLazada(fallback); + } + + return new LazadaPlatformOptions + { + AppKey = Coalesce(entity.AppKey, fallback.AppKey), + AppSecret = Coalesce(entity.AppSecret, fallback.AppSecret), + AuthorizeUrl = Coalesce(entity.BaseUrl, fallback.AuthorizeUrl), + AccessTokenUrl = Coalesce(entity.Remark, fallback.AccessTokenUrl) + }; + } + + private static ShopeePlatformOptions MergeShopee(SystemOtherConfig? entity, ShopeePlatformOptions fallback) + { + fallback ??= new ShopeePlatformOptions(); + if (entity == null || !entity.Enabled) + { + return CloneShopee(fallback); + } + + var partnerId = fallback.PartnerId; + if (int.TryParse(entity.AppKey?.Trim(), out var parsed) && parsed > 0) + { + partnerId = parsed; + } + + return new ShopeePlatformOptions + { + PartnerId = partnerId, + PartnerKey = Coalesce(entity.AppSecret, fallback.PartnerKey), + HostAuthUrl = Coalesce(entity.BaseUrl, fallback.HostAuthUrl), + HostUrl = Coalesce(entity.Remark, fallback.HostUrl) + }; + } + + private static TiktokPlatformOptions MergeTiktok(SystemOtherConfig? entity, TiktokPlatformOptions fallback) + { + fallback ??= new TiktokPlatformOptions(); + if (entity == null || !entity.Enabled) + { + return CloneTiktok(fallback); + } + + return new TiktokPlatformOptions + { + AppKey = Coalesce(entity.AppKey, fallback.AppKey), + AppSecret = Coalesce(entity.AppSecret, fallback.AppSecret), + HostAuth = Coalesce(entity.BaseUrl, fallback.HostAuth), + HostApi = Coalesce(entity.Remark, fallback.HostApi) + }; + } + + private static TemuPlatformOptions MergeTemu(SystemOtherConfig? entity, TemuPlatformOptions fallback) + { + fallback ??= new TemuPlatformOptions(); + if (entity == null || !entity.Enabled) + { + return CloneTemu(fallback); + } + + return new TemuPlatformOptions + { + AppKey = Coalesce(entity.AppKey, fallback.AppKey), + AppSecret = Coalesce(entity.AppSecret, fallback.AppSecret), + HostAuthUrl = Coalesce(entity.BaseUrl, fallback.HostAuthUrl) + }; + } + + private static LazadaPlatformOptions CloneLazada(LazadaPlatformOptions src) => new() + { + AppKey = src.AppKey, + AppSecret = src.AppSecret, + AuthorizeUrl = src.AuthorizeUrl, + AccessTokenUrl = src.AccessTokenUrl + }; + + private static ShopeePlatformOptions CloneShopee(ShopeePlatformOptions src) => new() + { + PartnerId = src.PartnerId, + PartnerKey = src.PartnerKey, + HostUrl = src.HostUrl, + HostAuthUrl = src.HostAuthUrl + }; + + private static TiktokPlatformOptions CloneTiktok(TiktokPlatformOptions src) => new() + { + AppKey = src.AppKey, + AppSecret = src.AppSecret, + HostAuth = src.HostAuth, + HostApi = src.HostApi + }; + + private static TemuPlatformOptions CloneTemu(TemuPlatformOptions src) => new() + { + AppKey = src.AppKey, + AppSecret = src.AppSecret, + HostAuthUrl = src.HostAuthUrl + }; + + private static string Coalesce(string? primary, string? fallback) => + !string.IsNullOrWhiteSpace(primary) ? primary.Trim() : (fallback ?? string.Empty); +} diff --git a/src/Oms.Application/Integrations/PlatformShop/PlatformShopServiceExtensions.cs b/src/Oms.Application/Integrations/PlatformShop/PlatformShopServiceExtensions.cs index bfde03d..0d618ab 100644 --- a/src/Oms.Application/Integrations/PlatformShop/PlatformShopServiceExtensions.cs +++ b/src/Oms.Application/Integrations/PlatformShop/PlatformShopServiceExtensions.cs @@ -14,6 +14,7 @@ public static class PlatformShopServiceExtensions { client.Timeout = TimeSpan.FromSeconds(60); }); + services.AddScoped(); services.AddScoped(); return services; } diff --git a/src/Oms.Application/Services/Orders/PlatformStoreService.cs b/src/Oms.Application/Services/Orders/PlatformStoreService.cs index f2a08c6..3244f7e 100644 --- a/src/Oms.Application/Services/Orders/PlatformStoreService.cs +++ b/src/Oms.Application/Services/Orders/PlatformStoreService.cs @@ -2,7 +2,6 @@ using System.Linq.Expressions; using Fake.Application; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Configuration; -using Microsoft.Extensions.Options; using Oms.Application.Dtos.PlatformStore; using Oms.Application.Dtos.Shared; using Oms.Application.Integrations.PlatformShop; @@ -23,8 +22,8 @@ public class PlatformStoreService : ApplicationService private ICustomerRepository CustomerRepository => LazyServiceProvider.GetRequiredService(); private ISalesStoreRepository StoreRepository => LazyServiceProvider.GetRequiredService(); private IPlatformShopApiClient PlatformShopApi => LazyServiceProvider.GetRequiredService(); - private PlatformShopOptions PlatformShopOptions => - LazyServiceProvider.GetRequiredService>().Value; + private IPlatformShopConfigProvider PlatformShopConfig => + LazyServiceProvider.GetRequiredService(); private IConfiguration Configuration => LazyServiceProvider.GetRequiredService(); [HttpPost("/order/platform-store/list")] @@ -71,7 +70,7 @@ public class PlatformStoreService : ApplicationService return platform switch { "temu" => await CreateTemuStoreAsync(customerId, nickname, input), - "lazada" or "shopee" or "tiktok" => BuildOAuthUrl(customerId, platform, nickname, input), + "lazada" or "shopee" or "tiktok" => await BuildOAuthUrlAsync(customerId, platform, nickname, input), _ => throw new BusinessException($"暂不支持平台:{platform}") }; } @@ -100,7 +99,13 @@ public class PlatformStoreService : ApplicationService var nickname = (input.StoreNickname ?? string.Empty).Trim(); var siteCountry = input.SiteCountry?.Trim(); - var stateSecret = ResolveStateSecret(); + var stateSecret = ResolveStateSecretFallback(); + var shopOptions = await PlatformShopConfig.GetAsync(); + if (!string.IsNullOrWhiteSpace(shopOptions.StateSecret)) + { + stateSecret = shopOptions.StateSecret; + } + if (PlatformShopAuthState.TryDecode(input.State, stateSecret, out var statePayload) && statePayload.CustomerId == customerId) { @@ -277,16 +282,21 @@ public class PlatformStoreService : ApplicationService }; } - private PlatformStoreAuthUrlOutput BuildOAuthUrl( + private async Task BuildOAuthUrlAsync( int customerId, string platform, string nickname, PlatformStoreAuthUrlInput input) { + var shopOptions = await PlatformShopConfig.GetAsync(); var redirect = string.IsNullOrWhiteSpace(input.RedirectUri) - ? $"{PlatformShopOptions.FrontendBaseUrl.TrimEnd('/')}/oauth/platform-store/{platform}" + ? $"{shopOptions.FrontendBaseUrl.TrimEnd('/')}/oauth/platform-store/{platform}" : input.RedirectUri.Trim(); + var stateSecret = !string.IsNullOrWhiteSpace(shopOptions.StateSecret) + ? shopOptions.StateSecret + : ResolveStateSecretFallback(); + var state = PlatformShopAuthState.Encode(new PlatformShopAuthStatePayload { CustomerId = customerId, @@ -294,11 +304,11 @@ public class PlatformStoreService : ApplicationService StoreNickname = nickname, SiteCountry = input.SiteCountry?.Trim(), Exp = DateTimeOffset.UtcNow.AddHours(2).ToUnixTimeSeconds() - }, ResolveStateSecret()); + }, stateSecret); try { - var authUrl = PlatformShopApi.BuildAuthUrl(platform, redirect, state, input.SiteCountry); + var authUrl = await PlatformShopApi.BuildAuthUrlAsync(platform, redirect, state, input.SiteCountry); return new PlatformStoreAuthUrlOutput { AuthUrl = authUrl, @@ -374,13 +384,8 @@ public class PlatformStoreService : ApplicationService return entity; } - private string ResolveStateSecret() + private string ResolveStateSecretFallback() { - if (!string.IsNullOrWhiteSpace(PlatformShopOptions.StateSecret)) - { - return PlatformShopOptions.StateSecret; - } - return Configuration["Jwt:SecretKey"] ?? Configuration["JwtSettings:SecretKey"] ?? "oms-platform-shop-state-secret"; diff --git a/src/Oms.Application/Services/System/SystemOtherConfigService.cs b/src/Oms.Application/Services/System/SystemOtherConfigService.cs index 0be5154..b2820b1 100644 --- a/src/Oms.Application/Services/System/SystemOtherConfigService.cs +++ b/src/Oms.Application/Services/System/SystemOtherConfigService.cs @@ -188,8 +188,44 @@ public class SystemOtherConfigService : ApplicationService throw new BusinessException("微信公众号 AppSecret 不能为空"); } } + else if (string.Equals(configCode, SystemOtherConfigCodes.PlatformShop, StringComparison.OrdinalIgnoreCase)) + { + if (string.IsNullOrWhiteSpace(input.BaseUrl)) + { + throw new BusinessException("店铺授权前端回调根地址不能为空"); + } + } + else if (IsPlatformShopAppConfig(configCode)) + { + if (string.IsNullOrWhiteSpace(input.AppKey)) + { + throw new BusinessException("平台 AppKey / PartnerId 不能为空"); + } + + if (excludeId == null && ShouldKeepExistingSecret(input.AppSecret)) + { + throw new BusinessException("平台 AppSecret / PartnerKey 不能为空"); + } + + if (string.Equals(configCode, SystemOtherConfigCodes.PlatformShopShopee, StringComparison.OrdinalIgnoreCase) + && !int.TryParse(input.AppKey.Trim(), out _)) + { + throw new BusinessException("Shopee PartnerId 必须为数字"); + } + + if (string.IsNullOrWhiteSpace(input.BaseUrl)) + { + throw new BusinessException("平台授权/API 地址不能为空"); + } + } } + private static bool IsPlatformShopAppConfig(string configCode) => + string.Equals(configCode, SystemOtherConfigCodes.PlatformShopLazada, StringComparison.OrdinalIgnoreCase) + || string.Equals(configCode, SystemOtherConfigCodes.PlatformShopShopee, StringComparison.OrdinalIgnoreCase) + || string.Equals(configCode, SystemOtherConfigCodes.PlatformShopTiktok, StringComparison.OrdinalIgnoreCase) + || string.Equals(configCode, SystemOtherConfigCodes.PlatformShopTemu, StringComparison.OrdinalIgnoreCase); + private static SystemOtherConfigOutput MapOutput(SystemOtherConfig entity, bool maskSecret) { return new SystemOtherConfigOutput diff --git a/src/Oms.Application/Shared/WarehouseCustomerHelper.cs b/src/Oms.Application/Shared/WarehouseCustomerHelper.cs index 8afa976..a05941a 100644 --- a/src/Oms.Application/Shared/WarehouseCustomerHelper.cs +++ b/src/Oms.Application/Shared/WarehouseCustomerHelper.cs @@ -35,7 +35,7 @@ public static class WarehouseCustomerHelper var customerId = await ResolveListCustomerIdAsync(customerRepository, currentUser, requestedCustomerId); if (!customerId.HasValue || customerId.Value <= 0) { - throw new BusinessException("客户ID不能为空(管理端请先在「我的包裹」选择客户后再使用采集插件)"); + throw new BusinessException("客户ID不能为空(管理端请先选择客户)"); } return customerId.Value; diff --git a/src/Oms.Domain/SystemAggregate/Consts/SystemOtherConfigCodes.cs b/src/Oms.Domain/SystemAggregate/Consts/SystemOtherConfigCodes.cs index 4363b2d..35ae9b6 100644 --- a/src/Oms.Domain/SystemAggregate/Consts/SystemOtherConfigCodes.cs +++ b/src/Oms.Domain/SystemAggregate/Consts/SystemOtherConfigCodes.cs @@ -14,4 +14,29 @@ public static class SystemOtherConfigCodes /// 微信公众号配置(AppKey=AppId,AppSecret=Secret,Remark=Token,BaseUrl=OAuthRedirectBase)。 /// public const string WeChatMp = "wechat_mp"; + + /// + /// 店铺授权公共配置(BaseUrl=前端回调根地址 FrontendBaseUrl,Remark=StateSecret 可选)。 + /// + public const string PlatformShop = "platform_shop"; + + /// + /// Lazada(AppKey/AppSecret,BaseUrl=AuthorizeUrl,Remark=AccessTokenUrl)。 + /// + public const string PlatformShopLazada = "platform_shop_lazada"; + + /// + /// Shopee(AppKey=PartnerId,AppSecret=PartnerKey,BaseUrl=HostAuthUrl,Remark=HostUrl)。 + /// + public const string PlatformShopShopee = "platform_shop_shopee"; + + /// + /// TikTok(AppKey/AppSecret,BaseUrl=HostAuth,Remark=HostApi)。 + /// + public const string PlatformShopTiktok = "platform_shop_tiktok"; + + /// + /// Temu(AppKey/AppSecret,BaseUrl=HostAuthUrl)。 + /// + public const string PlatformShopTemu = "platform_shop_temu"; }