diff --git a/sql/migrate_wechat_mp_other_config.sql b/sql/migrate_wechat_mp_other_config.sql
new file mode 100644
index 0000000..4e4c778
--- /dev/null
+++ b/sql/migrate_wechat_mp_other_config.sql
@@ -0,0 +1,31 @@
+-- 微信公众号配置(t_system_other_config)
+-- 字段映射:AppKey=AppId,AppSecret=Secret,Remark=Token,BaseUrl=OAuthRedirectBase
+-- 请在管理端「系统 → 其他配置」填写真实 AppId / AppSecret / Token,勿将密钥提交到 Git。
+
+INSERT INTO `t_system_other_config` (
+ `ConfigCode`,
+ `ConfigName`,
+ `Enabled`,
+ `BaseUrl`,
+ `AppKey`,
+ `AppSecret`,
+ `Remark`,
+ `TimeoutSeconds`,
+ `IsDeleted`,
+ `CreateUserId`
+)
+SELECT
+ 'wechat_mp',
+ '微信公众号',
+ 1,
+ 'https://oms.nanafa.com',
+ '',
+ '',
+ '',
+ 30,
+ 0,
+ 0
+FROM DUAL
+WHERE NOT EXISTS (
+ SELECT 1 FROM `t_system_other_config` WHERE `ConfigCode` = 'wechat_mp' AND `IsDeleted` = 0
+);
diff --git a/src/Oms.Application/Services/System/SystemOtherConfigService.cs b/src/Oms.Application/Services/System/SystemOtherConfigService.cs
index 7f88981..1535b23 100644
--- a/src/Oms.Application/Services/System/SystemOtherConfigService.cs
+++ b/src/Oms.Application/Services/System/SystemOtherConfigService.cs
@@ -163,6 +163,23 @@ public class SystemOtherConfigService : ApplicationService
throw new BusinessException("轨迹服务 AppSecret 不能为空");
}
}
+ else if (string.Equals(configCode, SystemOtherConfigCodes.WeChatMp, StringComparison.OrdinalIgnoreCase))
+ {
+ if (string.IsNullOrWhiteSpace(input.AppKey))
+ {
+ throw new BusinessException("微信公众号 AppId 不能为空");
+ }
+
+ if (string.IsNullOrWhiteSpace(input.Remark))
+ {
+ throw new BusinessException("微信公众号 Token 不能为空");
+ }
+
+ if (excludeId == null && ShouldKeepExistingSecret(input.AppSecret))
+ {
+ throw new BusinessException("微信公众号 AppSecret 不能为空");
+ }
+ }
}
private static SystemOtherConfigOutput MapOutput(SystemOtherConfig entity, bool maskSecret)
diff --git a/src/Oms.Domain/SystemAggregate/Consts/SystemOtherConfigCodes.cs b/src/Oms.Domain/SystemAggregate/Consts/SystemOtherConfigCodes.cs
index f549e00..4363b2d 100644
--- a/src/Oms.Domain/SystemAggregate/Consts/SystemOtherConfigCodes.cs
+++ b/src/Oms.Domain/SystemAggregate/Consts/SystemOtherConfigCodes.cs
@@ -9,4 +9,9 @@ public static class SystemOtherConfigCodes
/// 小包轨迹服务配置。
///
public const string TrackService = "track_service";
+
+ ///
+ /// 微信公众号配置(AppKey=AppId,AppSecret=Secret,Remark=Token,BaseUrl=OAuthRedirectBase)。
+ ///
+ public const string WeChatMp = "wechat_mp";
}
diff --git a/src/SimpleAdmin.Api.IntegrationTests/WeChatRouteIntegrationTests.cs b/src/SimpleAdmin.Api.IntegrationTests/WeChatRouteIntegrationTests.cs
index bb07084..205ac8b 100644
--- a/src/SimpleAdmin.Api.IntegrationTests/WeChatRouteIntegrationTests.cs
+++ b/src/SimpleAdmin.Api.IntegrationTests/WeChatRouteIntegrationTests.cs
@@ -3,6 +3,11 @@ using System.Security.Cryptography;
using System.Text;
using FluentAssertions;
using Microsoft.Extensions.DependencyInjection;
+using Microsoft.Extensions.DependencyInjection.Extensions;
+using Moq;
+using Oms.Domain.SystemAggregate.Consts;
+using Oms.Domain.SystemAggregate.Entities;
+using Oms.Domain.SystemAggregate.Repositories;
using SimpleAdmin.Api.IntegrationTests.Support;
using SimpleAdmin.Api.WeChat.Options;
@@ -13,14 +18,7 @@ public class WeChatRouteIntegrationTests
[Fact]
public async Task WeChatRoutes_ShouldBeRegisteredOnAuthService()
{
- await using var host = await SimpleAdminApiTestHost.CreateAsync(configureServices: services =>
- {
- services.PostConfigure(options =>
- {
- options.Token = "test-token";
- options.AppId = "wx-test";
- });
- });
+ await using var host = await SimpleAdminApiTestHost.CreateAsync(configureServices: ConfigureWeChatFallback);
host.RoutePatterns.Should().Contain("apiwechat/verification");
host.RoutePatterns.Should().Contain("wechat/mp/{appId}");
@@ -32,6 +30,7 @@ public class WeChatRouteIntegrationTests
{
await using var host = await SimpleAdminApiTestHost.CreateAsync(configureServices: services =>
{
+ ConfigureWeChatFallback(services);
services.PostConfigure(options => options.Token = "test-token");
});
@@ -47,6 +46,22 @@ public class WeChatRouteIntegrationTests
(await response.Content.ReadAsStringAsync()).Should().Be(echostr);
}
+ private static void ConfigureWeChatFallback(IServiceCollection services)
+ {
+ services.PostConfigure(options =>
+ {
+ options.Token = "test-token";
+ options.AppId = "wx-test";
+ });
+
+ var repository = new Mock();
+ repository
+ .Setup(x => x.GetByConfigCodeAsync(SystemOtherConfigCodes.WeChatMp, It.IsAny()))
+ .ReturnsAsync((SystemOtherConfig?)null);
+ services.RemoveAll();
+ services.AddSingleton(repository.Object);
+ }
+
private static string SignWeChat(string token, string timestamp, string nonce)
{
var parts = new[] { token, timestamp, nonce }.OrderBy(x => x, StringComparer.Ordinal).ToArray();
diff --git a/src/SimpleAdmin.Api/Auth/AuthService.WeChatMp.cs b/src/SimpleAdmin.Api/Auth/AuthService.WeChatMp.cs
index 2265077..df57e47 100644
--- a/src/SimpleAdmin.Api/Auth/AuthService.WeChatMp.cs
+++ b/src/SimpleAdmin.Api/Auth/AuthService.WeChatMp.cs
@@ -2,18 +2,16 @@ using Fake.Application;
using Fake.Domain.Exceptions;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Options;
using SimpleAdmin.Api.WeChat;
using SimpleAdmin.Api.WeChat.Dtos;
-using SimpleAdmin.Api.WeChat.Options;
using SimpleAdmin.Api.WeChat.Services;
namespace SimpleAdmin.Api.Auth;
public partial class AuthService
{
- private IOptions WeChatMpOptions =>
- LazyServiceProvider.GetRequiredService>();
+ private IWeChatMpConfigProvider WeChatMpConfigProvider =>
+ LazyServiceProvider.GetRequiredService();
private IWeChatMpApiClient WeChatMpApiClient =>
LazyServiceProvider.GetRequiredService();
@@ -31,12 +29,16 @@ public partial class AuthService
[HttpGet("/apiwechat/verification")]
[Produces("text/plain")]
[ApiExplorerSettings(GroupName = "WeChat")]
- public ContentResult WeChatValidateLegacyAsync(
+ public async Task WeChatValidateLegacyAsync(
[FromQuery] string? signature,
[FromQuery] string? timestamp,
[FromQuery] string? nonce,
- [FromQuery] string? echostr)
- => WeChatMpCallbackSupport.Validate(WeChatMpOptions, signature, timestamp, nonce, echostr);
+ [FromQuery] string? echostr,
+ CancellationToken cancellationToken = default)
+ {
+ var options = await WeChatMpConfigProvider.GetAsync(cancellationToken);
+ return WeChatMpCallbackSupport.Validate(options, signature, timestamp, nonce, echostr);
+ }
///
/// 微信服务器接入验签。
@@ -45,15 +47,17 @@ public partial class AuthService
[HttpGet("/wechat/mp/{appId}")]
[Produces("text/plain")]
[ApiExplorerSettings(GroupName = "WeChat")]
- public ContentResult WeChatValidateAsync(
+ public async Task WeChatValidateAsync(
[FromRoute] string appId,
[FromQuery] string? signature,
[FromQuery] string? timestamp,
[FromQuery] string? nonce,
- [FromQuery] string? echostr)
+ [FromQuery] string? echostr,
+ CancellationToken cancellationToken = default)
{
- WeChatMpCallbackSupport.EnsureAppId(WeChatMpOptions, appId);
- return WeChatMpCallbackSupport.Validate(WeChatMpOptions, signature, timestamp, nonce, echostr);
+ var options = await WeChatMpConfigProvider.GetAsync(cancellationToken);
+ WeChatMpCallbackSupport.EnsureAppId(options, appId);
+ return WeChatMpCallbackSupport.Validate(options, signature, timestamp, nonce, echostr);
}
///
@@ -64,17 +68,18 @@ public partial class AuthService
[Consumes("text/xml", "application/xml", "text/plain")]
[Produces("application/xml")]
[ApiExplorerSettings(GroupName = "WeChat")]
- public Task WeChatMessageLegacyAsync(
+ public async Task WeChatMessageLegacyAsync(
[FromQuery] string signature,
[FromQuery] string timestamp,
[FromQuery] string nonce,
[FromQuery(Name = "encrypt_type")] string? encryptType,
CancellationToken cancellationToken = default)
{
+ var options = await WeChatMpConfigProvider.GetAsync(cancellationToken);
var request = httpContextAccessor.HttpContext?.Request
?? throw new BusinessException("无法读取微信回调请求");
- return WeChatMpCallbackSupport.HandleMessageAsync(
- WeChatMpOptions,
+ return await WeChatMpCallbackSupport.HandleMessageAsync(
+ options,
WeChatEventHandler,
request,
WeChatLogger,
@@ -101,11 +106,12 @@ public partial class AuthService
[FromQuery(Name = "encrypt_type")] string? encryptType,
CancellationToken cancellationToken = default)
{
- WeChatMpCallbackSupport.EnsureAppId(WeChatMpOptions, appId);
+ var options = await WeChatMpConfigProvider.GetAsync(cancellationToken);
+ WeChatMpCallbackSupport.EnsureAppId(options, appId);
var request = httpContextAccessor.HttpContext?.Request
?? throw new BusinessException("无法读取微信回调请求");
return await WeChatMpCallbackSupport.HandleMessageAsync(
- WeChatMpOptions,
+ options,
WeChatEventHandler,
request,
WeChatLogger,
diff --git a/src/SimpleAdmin.Api/WeChat/Services/WeChatMpApiClient.cs b/src/SimpleAdmin.Api/WeChat/Services/WeChatMpApiClient.cs
index 126df39..60fbccb 100644
--- a/src/SimpleAdmin.Api/WeChat/Services/WeChatMpApiClient.cs
+++ b/src/SimpleAdmin.Api/WeChat/Services/WeChatMpApiClient.cs
@@ -1,7 +1,6 @@
using System.Text.Json.Serialization;
using System.Xml.Linq;
using Microsoft.Extensions.Caching.Memory;
-using Microsoft.Extensions.Options;
using SimpleAdmin.Api.WeChat.Options;
namespace SimpleAdmin.Api.WeChat.Services;
@@ -30,7 +29,7 @@ public sealed record WeChatOAuthUserResult(
internal sealed class WeChatMpApiClient(
IHttpClientFactory httpClientFactory,
IMemoryCache memoryCache,
- IOptions options,
+ IWeChatMpConfigProvider configProvider,
ILogger logger) : IWeChatMpApiClient
{
private const string AccessTokenCacheKey = "oms:wechat:mp:access_token";
@@ -78,7 +77,7 @@ internal sealed class WeChatMpApiClient(
public async Task GetOAuthAccessTokenAsync(string code, CancellationToken cancellationToken = default)
{
- var mp = options.Value;
+ var mp = await configProvider.GetAsync(cancellationToken);
var client = httpClientFactory.CreateClient(nameof(WeChatMpApiClient));
var url =
$"https://api.weixin.qq.com/sns/oauth2/access_token?appid={Uri.EscapeDataString(mp.AppId)}&secret={Uri.EscapeDataString(mp.Secret)}&code={Uri.EscapeDataString(code)}&grant_type=authorization_code";
@@ -131,7 +130,7 @@ internal sealed class WeChatMpApiClient(
return cached;
}
- var mp = options.Value;
+ var mp = await configProvider.GetAsync(cancellationToken);
var client = httpClientFactory.CreateClient(nameof(WeChatMpApiClient));
var url =
$"https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid={Uri.EscapeDataString(mp.AppId)}&secret={Uri.EscapeDataString(mp.Secret)}";
diff --git a/src/SimpleAdmin.Api/WeChat/Services/WeChatMpConfigProvider.cs b/src/SimpleAdmin.Api/WeChat/Services/WeChatMpConfigProvider.cs
new file mode 100644
index 0000000..135994a
--- /dev/null
+++ b/src/SimpleAdmin.Api/WeChat/Services/WeChatMpConfigProvider.cs
@@ -0,0 +1,42 @@
+using Fake.DependencyInjection;
+using Microsoft.Extensions.Options;
+using Oms.Domain.SystemAggregate.Consts;
+using Oms.Domain.SystemAggregate.Repositories;
+using SimpleAdmin.Api.WeChat.Options;
+
+namespace SimpleAdmin.Api.WeChat.Services;
+
+///
+/// 微信公众号运行时配置:优先读 t_system_other_config(wechat_mp),缺省回退 Consul/appsettings。
+///
+public interface IWeChatMpConfigProvider
+{
+ Task GetAsync(CancellationToken cancellationToken = default);
+}
+
+internal sealed class WeChatMpConfigProvider(
+ ISystemOtherConfigRepository repository,
+ IOptions options) : IWeChatMpConfigProvider, IScopedDependency
+{
+ public async Task GetAsync(CancellationToken cancellationToken = default)
+ {
+ var fallback = options.Value;
+ var entity = await repository.GetByConfigCodeAsync(SystemOtherConfigCodes.WeChatMp, cancellationToken);
+ if (entity == null)
+ {
+ return fallback;
+ }
+
+ return new WeChatMpOptions
+ {
+ AppId = Coalesce(entity.AppKey, fallback.AppId),
+ Secret = Coalesce(entity.AppSecret, fallback.Secret),
+ Token = Coalesce(entity.Remark, fallback.Token),
+ OAuthRedirectBase = Coalesce(entity.BaseUrl, fallback.OAuthRedirectBase),
+ EncodingAesKey = fallback.EncodingAesKey
+ };
+ }
+
+ private static string Coalesce(string? primary, string fallback) =>
+ string.IsNullOrWhiteSpace(primary) ? fallback : primary.Trim();
+}
diff --git a/src/SimpleAdmin.Api/WeChat/WeChatMpCallbackSupport.cs b/src/SimpleAdmin.Api/WeChat/WeChatMpCallbackSupport.cs
index 98d0ef3..4d3d126 100644
--- a/src/SimpleAdmin.Api/WeChat/WeChatMpCallbackSupport.cs
+++ b/src/SimpleAdmin.Api/WeChat/WeChatMpCallbackSupport.cs
@@ -1,6 +1,5 @@
using Fake.Application;
using Microsoft.AspNetCore.Mvc;
-using Microsoft.Extensions.Options;
using SimpleAdmin.Api.WeChat.Options;
using SimpleAdmin.Api.WeChat.Services;
@@ -9,14 +8,14 @@ namespace SimpleAdmin.Api.WeChat;
internal static class WeChatMpCallbackSupport
{
public static ContentResult Validate(
- IOptions options,
+ WeChatMpOptions options,
string? signature,
string? timestamp,
string? nonce,
string? echostr)
{
// 微信服务器要求 HTTP 200 + 纯文本 echostr,不可抛 BusinessException(会变成 JSON 非 200)
- if (string.IsNullOrWhiteSpace(options.Value.Token))
+ if (string.IsNullOrWhiteSpace(options.Token))
{
return PlainText("error");
}
@@ -29,12 +28,12 @@ internal static class WeChatMpCallbackSupport
return PlainText("error");
}
- var valid = WeChatSignatureHelper.Validate(options.Value.Token, timestamp, nonce, signature);
+ var valid = WeChatSignatureHelper.Validate(options.Token, timestamp, nonce, signature);
return PlainText(valid ? echostr : "error");
}
public static async Task HandleMessageAsync(
- IOptions options,
+ WeChatMpOptions options,
IWeChatEventHandler eventHandler,
HttpRequest request,
ILogger logger,
@@ -44,7 +43,7 @@ internal static class WeChatMpCallbackSupport
string? encryptType,
CancellationToken cancellationToken)
{
- if (!WeChatSignatureHelper.Validate(options.Value.Token, timestamp, nonce, signature))
+ if (!WeChatSignatureHelper.Validate(options.Token, timestamp, nonce, signature))
{
logger.LogWarning("微信消息验签失败");
return PlainText("error");
@@ -76,9 +75,9 @@ internal static class WeChatMpCallbackSupport
return Xml(reply ?? string.Empty);
}
- public static void EnsureAppId(IOptions options, string appId)
+ public static void EnsureAppId(WeChatMpOptions options, string appId)
{
- var configured = options.Value.AppId;
+ var configured = options.AppId;
if (string.IsNullOrWhiteSpace(configured))
{
throw new BusinessException("未配置微信公众号 AppId");
diff --git a/src/SimpleAdmin.Api/WeChat/WeChatServiceExtensions.cs b/src/SimpleAdmin.Api/WeChat/WeChatServiceExtensions.cs
index 6213e1d..0009fdd 100644
--- a/src/SimpleAdmin.Api/WeChat/WeChatServiceExtensions.cs
+++ b/src/SimpleAdmin.Api/WeChat/WeChatServiceExtensions.cs
@@ -23,7 +23,7 @@ public static class WeChatServiceExtensions
services.AddDistributedMemoryCache();
}
- services.AddSingleton();
+ services.AddScoped();
services.AddSingleton();
services.AddTransient();