using Fake.Application;
using Microsoft.AspNetCore.Mvc;
using SimpleAdmin.Api.WeChat.Options;
using SimpleAdmin.Api.WeChat.Services;
namespace SimpleAdmin.Api.WeChat;
internal static class WeChatMpCallbackSupport
{
///
/// 微信 GET 接入验签:从 Query 读取参数,验签通过后原样输出 echostr。
///
public static bool TryValidateGetRequest(
WeChatMpOptions options,
HttpRequest request,
ILogger? logger,
out string echostr)
{
echostr = string.Empty;
if (string.IsNullOrWhiteSpace(options.Token))
{
logger?.LogWarning("微信 GET 验签失败:未配置 Token");
return false;
}
var signature = request.Query["signature"].ToString();
var timestamp = request.Query["timestamp"].ToString();
var nonce = request.Query["nonce"].ToString();
echostr = request.Query["echostr"].ToString();
if (string.IsNullOrWhiteSpace(signature)
|| string.IsNullOrWhiteSpace(timestamp)
|| string.IsNullOrWhiteSpace(nonce)
|| string.IsNullOrEmpty(echostr))
{
logger?.LogWarning(
"微信 GET 验签失败:缺少必要参数 signature={HasSig} timestamp={HasTs} nonce={HasNonce} echostr={HasEcho}",
!string.IsNullOrWhiteSpace(signature),
!string.IsNullOrWhiteSpace(timestamp),
!string.IsNullOrWhiteSpace(nonce),
!string.IsNullOrEmpty(echostr));
return false;
}
if (!WeChatSignatureHelper.Validate(options.Token, timestamp, nonce, signature))
{
logger?.LogWarning(
"微信 GET 验签失败:signature 不匹配 timestamp={Timestamp} nonce={Nonce}",
timestamp,
nonce);
return false;
}
return true;
}
public static ContentResult Validate(
WeChatMpOptions options,
HttpRequest request,
string? signature,
string? timestamp,
string? nonce)
{
if (!TryValidateGetRequest(options, request, null, out var echostr))
{
return PlainError();
}
return EchoStr(echostr);
}
/// 微信或探活可能发 HEAD,须返回 200(WMS/Spring 默认支持)。
public static ContentResult HeadOk() =>
new() { Content = string.Empty, ContentType = "text/plain; charset=utf-8", StatusCode = StatusCodes.Status200OK };
public static ContentResult PlainError() => PlainText("error");
public static async Task HandleMessageAsync(
WeChatMpOptions options,
IWeChatEventHandler eventHandler,
HttpRequest request,
ILogger logger,
string signature,
string timestamp,
string nonce,
string? encryptType,
CancellationToken cancellationToken)
{
if (string.IsNullOrWhiteSpace(options.Token))
{
logger.LogWarning("微信消息回调未配置 Token");
return PlainText("error");
}
// token、timestamp、nonce 字典序排序后拼接,SHA1 与 signature 对比
if (!WeChatSignatureHelper.Validate(options.Token, timestamp, nonce, signature))
{
logger.LogWarning("微信消息验签失败");
return PlainText("error");
}
using var reader = new StreamReader(request.Body);
var requestBody = await reader.ReadToEndAsync(cancellationToken);
if (string.IsNullOrWhiteSpace(requestBody))
{
return AckSuccess();
}
// 明文模式:无 encrypt_type 或 encrypt_type=raw;安全模式 AES 暂不支持
if (!string.IsNullOrWhiteSpace(encryptType)
&& !string.Equals(encryptType, "raw", StringComparison.OrdinalIgnoreCase))
{
logger.LogWarning(
"收到加密微信消息({EncryptType}),当前仅支持明文模式,请在公众平台切换为明文或后续启用 AES。",
encryptType);
return AckSuccess();
}
var message = WeChatIncomingMessageParser.Parse(requestBody, request.ContentType);
if (message == null)
{
logger.LogWarning("无法解析微信消息体(支持明文 XML / JSON)");
return AckSuccess();
}
var reply = await eventHandler.HandleAsync(message, cancellationToken);
return BuildMessageReply(reply);
}
public static void EnsureAppId(WeChatMpOptions options, string appId)
{
var configured = options.AppId;
if (string.IsNullOrWhiteSpace(configured))
{
throw new BusinessException("未配置微信公众号 AppId");
}
if (!string.Equals(configured, appId, StringComparison.OrdinalIgnoreCase))
{
throw new BusinessException("公众号 AppId 不匹配");
}
}
private static ContentResult EchoStr(string echostr) =>
new() { Content = echostr, ContentType = "text/plain", StatusCode = StatusCodes.Status200OK };
private static ContentResult AckSuccess() => PlainText("success");
private static ContentResult BuildMessageReply(string? reply)
{
if (string.IsNullOrEmpty(reply))
{
return AckSuccess();
}
return reply.TrimStart().StartsWith('<') ? Xml(reply) : PlainText(reply);
}
private static ContentResult PlainText(string content) =>
new() { Content = content, ContentType = "text/plain; charset=utf-8", StatusCode = StatusCodes.Status200OK };
private static ContentResult Xml(string content) =>
new() { Content = content, ContentType = "application/xml; charset=utf-8", StatusCode = StatusCodes.Status200OK };
}