using Fake.Application; using Fake.Domain.Exceptions; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using SimpleAdmin.Api.WeChat; using SimpleAdmin.Api.WeChat.Dtos; using SimpleAdmin.Api.WeChat.Services; namespace SimpleAdmin.Api.Auth; public partial class AuthService { private IWeChatMpConfigProvider WeChatMpConfigProvider => LazyServiceProvider.GetRequiredService(); private IWeChatMpApiClient WeChatMpApiClient => LazyServiceProvider.GetRequiredService(); private IWeChatEventHandler WeChatEventHandler => LazyServiceProvider.GetRequiredService(); private ILogger WeChatLogger => LazyServiceProvider.GetRequiredService>(); /// /// 微信公众号接入验签,验签通过后原样返回 echostr。 /// [AllowAnonymous] [HttpGet("/apiwechat/verification")] [Produces("text/plain")] [ApiExplorerSettings(GroupName = "WeChat")] public Task WeChatValidateLegacyAsync( [FromQuery] string? signature, [FromQuery] string? timestamp, [FromQuery] string? nonce, [FromQuery] string? echostr, CancellationToken cancellationToken = default) => WeChatValidateCoreAsync(cancellationToken); private async Task WeChatValidateCoreAsync(CancellationToken cancellationToken) { var httpContext = httpContextAccessor.HttpContext; var request = httpContext?.Request; if (request == null) { return WeChatMpCallbackSupport.PlainError(); } try { var options = await WeChatMpConfigProvider.GetAsync(cancellationToken); if (!WeChatMpCallbackSupport.TryValidateGetRequest(options, request, WeChatLogger, out var echo)) { return WeChatMpCallbackSupport.PlainError(); } // 微信要求:HTTP 200 + 响应体仅为 echostr 原文,不可 JSON 包装 var response = httpContext!.Response; response.StatusCode = StatusCodes.Status200OK; response.ContentType = "text/plain"; await response.WriteAsync(echo, cancellationToken); return new EmptyResult(); } catch (Exception ex) { WeChatLogger.LogWarning(ex, "微信公众号验签读取配置失败"); return WeChatMpCallbackSupport.PlainError(); } } /// /// 微信服务器接入验签,验签通过后原样返回 echostr。 /// appId 仅匹配 wx 开头,避免 login-qrcode / oauth-config 等静态段落入本路由。 /// [AllowAnonymous] [HttpGet("/wechat/mp/{appId:regex(^wx[[a-zA-Z0-9]]+$)}")] [Produces("text/plain")] [ApiExplorerSettings(GroupName = "WeChat")] public async Task WeChatValidateAsync( [FromRoute] string appId, [FromQuery] string? signature, [FromQuery] string? timestamp, [FromQuery] string? nonce, [FromQuery] string? echostr, CancellationToken cancellationToken = default) { var httpContext = httpContextAccessor.HttpContext ?? throw new BusinessException("无法读取微信验签请求"); var options = await WeChatMpConfigProvider.GetAsync(cancellationToken); WeChatMpCallbackSupport.EnsureAppId(options, appId); if (!WeChatMpCallbackSupport.TryValidateGetRequest(options, httpContext.Request, WeChatLogger, out var echo)) { return WeChatMpCallbackSupport.PlainError(); } var response = httpContext.Response; response.StatusCode = StatusCodes.Status200OK; response.ContentType = "text/plain"; await response.WriteAsync(echo, cancellationToken); return new EmptyResult(); } /// /// 微信公众号消息/事件推送(明文,XML/JSON)。 /// [AllowAnonymous] [HttpPost("/apiwechat/verification")] [Consumes("text/xml", "application/xml", "application/json", "text/plain")] [Produces("text/plain", "application/xml")] [ApiExplorerSettings(GroupName = "WeChat")] public Task WeChatMessageLegacyAsync( [FromQuery] string signature, [FromQuery] string timestamp, [FromQuery] string nonce, [FromQuery(Name = "encrypt_type")] string? encryptType, CancellationToken cancellationToken = default) => WeChatMessageCoreAsync(signature, timestamp, nonce, encryptType, cancellationToken); /// /// 微信消息/事件推送(明文,XML/JSON)。 /// [AllowAnonymous] [HttpPost("/wechat/mp/{appId:regex(^wx[[a-zA-Z0-9]]+$)}")] [Consumes("text/xml", "application/xml", "application/json", "text/plain")] [Produces("text/plain", "application/xml")] [ApiExplorerSettings(GroupName = "WeChat")] public async Task WeChatMessageAsync( [FromRoute] string appId, [FromQuery] string signature, [FromQuery] string timestamp, [FromQuery] string nonce, [FromQuery(Name = "encrypt_type")] string? encryptType, CancellationToken cancellationToken = default) { var options = await WeChatMpConfigProvider.GetAsync(cancellationToken); WeChatMpCallbackSupport.EnsureAppId(options, appId); return await WeChatMessageCoreAsync(signature, timestamp, nonce, encryptType, cancellationToken); } private async Task WeChatMessageCoreAsync( string signature, string timestamp, string nonce, string? encryptType, CancellationToken cancellationToken) { var request = httpContextAccessor.HttpContext?.Request ?? throw new BusinessException("无法读取微信回调请求"); var options = await WeChatMpConfigProvider.GetAsync(cancellationToken); return await WeChatMpCallbackSupport.HandleMessageAsync( options, WeChatEventHandler, request, WeChatLogger, signature, timestamp, nonce, encryptType, cancellationToken); } /// /// H5 网页授权公开配置:AppId + OAuth 回调根(来自其他配置 wechat_mp)。 /// 注意:勿挂在 /wechat/mp/{appId} 下,避免被公众号回调路由吞掉。 /// [AllowAnonymous] [HttpGet("/wechat/oauth/config")] [ApiExplorerSettings(GroupName = "WeChat")] public async Task GetWeChatOAuthConfigAsync( CancellationToken cancellationToken = default) { var options = await WeChatMpConfigProvider.GetAsync(cancellationToken); if (string.IsNullOrWhiteSpace(options.AppId)) { throw new BusinessException( "未配置微信公众号 AppId。请在「其他配置」维护 wechat_mp,AppKey 填公众号 AppId。"); } return new WxOAuthConfigOutput { AppId = options.AppId.Trim(), OAuthRedirectBase = string.IsNullOrWhiteSpace(options.OAuthRedirectBase) ? null : options.OAuthRedirectBase.Trim().TrimEnd('/') }; } /// /// 获取 PC 扫码登录二维码。 /// [AllowAnonymous] [HttpGet("/wechat/mp/login-qrcode")] [ApiExplorerSettings(GroupName = "WeChat")] public async Task GetWeChatLoginQrcodeAsync( [FromQuery] string? loginUuid, CancellationToken cancellationToken = default) { var loginKey = string.IsNullOrWhiteSpace(loginUuid) ? Guid.NewGuid().ToString("N") : loginUuid.Trim(); const int expireSeconds = 600; var qrCodeUrl = await WeChatMpApiClient.CreateLoginQrCodeUrlAsync( loginKey, expireSeconds, cancellationToken); return new WxLoginQrcodeOutput { LoginKey = loginKey, QrCodeUrl = qrCodeUrl, ExpiresIn = expireSeconds }; } /// /// 获取注册/绑定成功后跳转的公众号链接(微信内直接打开即可关注,无需长按扫码)。 /// [AllowAnonymous] [HttpGet("/wechat/mp/follow-qrcode")] [ApiExplorerSettings(GroupName = "WeChat")] public async Task GetWeChatFollowQrcodeAsync( CancellationToken cancellationToken = default) { var jumpUrl = await WeChatMpConfigProvider.GetFollowJumpUrlAsync(cancellationToken); if (string.IsNullOrWhiteSpace(jumpUrl)) { throw new BusinessException( "未配置公众号关注跳转链接。请在「其他配置」新增 wechat_mp_follow,BaseUrl 填公众号主页/名片链接。"); } return new WxFollowQrcodeOutput { JumpUrl = jumpUrl, QrCodeUrl = jumpUrl, Scene = WeChatQrSceneParser.FollowScene }; } /// /// 同源代理微信 showqrcode 图片,便于微信内长按识别(避免跨域直连 mp.weixin.qq.com)。 /// [AllowAnonymous] [HttpGet("/wechat/mp/showqrcode")] [ApiExplorerSettings(GroupName = "WeChat")] public async Task GetWeChatShowQrCodeAsync( [FromQuery] string ticket, CancellationToken cancellationToken = default) { if (string.IsNullOrWhiteSpace(ticket)) { return new BadRequestObjectResult("ticket required"); } try { var (bytes, contentType) = await WeChatMpApiClient.DownloadShowQrCodeAsync( ticket, cancellationToken); // 短缓存:同一次 ticket 有效期内可复用,减少重复拉取 return new FileContentResult(bytes, contentType) { EnableRangeProcessing = false }; } catch (Exception ex) { WeChatLogger.LogWarning(ex, "代理下载微信登录二维码失败"); return new NotFoundObjectResult("qrcode not found"); } } }