问题场景

开发微信支付Native扫码支付时(模式二:动态二维码模式),用户扫码付款成功,但服务器一直没收到回调通知,导致订单状态无法自动更新为“已支付”。

本文环境:

  • 服务器:阿里云 ECS + Windows Server + IIS

  • 支付接口:微信支付 V3

  • 证书:阿里云免费证书(90天有效期)


一、排障核心思路

微信支付回调的完整链路是:

text

微信服务器 → TLS握手 → 阿里云安全组 → IIS → 应用程序代码 → 返回SUCCESS应答

任何一个环节出问题都会导致“收不到通知”。排障原则:先从微信侧确认失败原因,再顺着链路逐层向服务器排查。


二、第一步:直接找微信客服问失败原因

不要瞎猜,最快拿到准确原因的方式是直接问微信支付官方客服

  1. 登录 微信支付商户平台

  2. 右下角点击 “帮助中心” → “联系客服”

  3. 准备好 商户订单号(你自己的订单号)和 微信支付订单号(交易记录里可看到)

  4. 直接问:

“这笔订单用户已经支付成功,但我们服务器一直没收到回调通知。帮我看下这笔订单的通知发送情况,失败的具体原因是什么?是连接超时、TLS握手失败、证书问题,还是其他原因?”

微信客服系统里一定有详细的失败原因,这比你自己在商户平台到处找权限、翻菜单快得多。


三、第二步:检查TLS协议版本(PCI DSS合规)

微信支付要求服务器满足 PCI DSS 安全标准,必须禁用 TLS 1.0 和 TLS 1.1,只保留 TLS 1.2 及以上版本。

1. 用在线工具检测

访问 MySSL.com,输入你的回调域名进行检测。

关键看两个指标:

  • TLS 1.0 / TLS 1.1:必须显示 “不支持”

  • PCI DSS:必须显示 “合规”

如果显示 TLS 1.0 或 1.1 仍然支持,或者 PCI DSS 显示“不合规”,微信回调就会在握手阶段直接被拒绝,IIS 日志里不会留下任何记录。

2. Windows Server + IIS 修复方法

IIS 本身不控制 TLS 协议版本,需要通过 Windows 注册表配置 SCHANNEL。

以管理员身份打开 PowerShell,执行以下脚本:

powershell

$regPath = "HKLM:\SYSTEM\CurrentControlSet\Control\SecurityProviders\SCHANNEL\Protocols"

# 禁用 TLS 1.0
New-Item -Path "$regPath\TLS 1.0\Client" -Force | Out-Null
New-ItemProperty -Path "$regPath\TLS 1.0\Client" -Name "Enabled" -Value 0 -PropertyType DWORD -Force
New-Item -Path "$regPath\TLS 1.0\Server" -Force | Out-Null
New-ItemProperty -Path "$regPath\TLS 1.0\Server" -Name "Enabled" -Value 0 -PropertyType DWORD -Force

# 禁用 TLS 1.1
New-Item -Path "$regPath\TLS 1.1\Client" -Force | Out-Null
New-ItemProperty -Path "$regPath\TLS 1.1\Client" -Name "Enabled" -Value 0 -PropertyType DWORD -Force
New-Item -Path "$regPath\TLS 1.1\Server" -Force | Out-Null
New-ItemProperty -Path "$regPath\TLS 1.1\Server" -Name "Enabled" -Value 0 -PropertyType DWORD -Force

# 启用 TLS 1.2
New-Item -Path "$regPath\TLS 1.2\Client" -Force | Out-Null
New-ItemProperty -Path "$regPath\TLS 1.2\Client" -Name "Enabled" -Value 1 -PropertyType DWORD -Force
New-Item -Path "$regPath\TLS 1.2\Server" -Force | Out-Null
New-ItemProperty -Path "$regPath\TLS 1.2\Server" -Name "Enabled" -Value 1 -PropertyType DWORD -Force

执行完成后,必须重启服务器!

重启后,再次用 MySSL 检测,确认 TLS 1.0/1.1 已变为“不支持”,PCI DSS 变为“合规”。


四、第三步:检查阿里云ECS安全组

即使 TLS 配置正确,如果安全组没有放行,微信的请求也到不了你的服务器。

  1. 登录 阿里云控制台

  2. 进入 ECS 实例详情

  3. 点击 安全组 → 配置规则

  4. 检查 入方向 是否有允许 443 端口 的规则

快速验证方法: 临时添加一条入方向规则,放行 0.0.0.0/0(所有IP)到 443 端口。调通后,再改回仅允许微信支付服务器IP段


五、第四步:检查IIS日志,确认请求是否到达

IIS 日志默认路径:

text

C:\inetpub\logs\LogFiles\W3SVC1\

找到对应日期的日志文件,用文本编辑器打开,搜索你的回调URL路径(如 /notify)。

情况一:日志里完全搜不到

说明微信的请求根本没到达 IIS,问题在:

  • 阿里云安全组未放行 443 端口

  • TLS 握手失败(TLS 版本或证书问题)

  • 域名解析错误

情况二:日志里有记录,但状态码不是 200

HTTP 状态码 含义 原因
405 Method Not Allowed IIS 不允许对此 URL 发起 POST 请求
500 Internal Server Error 回调处理代码异常
404 Not Found 回调 URL 路径错误
401/403 未授权/禁止访问 回调 URL 被认证或授权拦截

六、第五步:重点解决 405 Method Not Allowed

如果 IIS 日志里出现了 405 错误,说明 TLS 和网络都没问题,但 IIS 不允许对这个 URL 发起 POST 请求。

常见原因及修复方法

原因一:回调URL指向了静态文件

如果 notify_url 写成了 .html.txt 等静态扩展名,IIS 默认只允许 GET 请求,POST 会直接返回 405。

修复: 将回调 URL 改为动态路径,如 /api/notify/notify.ashx 等。

原因二:WebDAV 模块拦截 POST 请求

IIS 上如果安装了 WebDAV Publishing 功能,它会抢在正常处理程序之前拦截 POST 请求。

修复: 在网站根目录的 web.config 中添加:

xml

<system.webServer>
  <modules>
    <remove name="WebDAVModule" />
  </modules>
  <handlers>
    <remove name="WebDAV" />
  </handlers>
</system.webServer>

如果没有使用 WebDAV,建议在“服务器管理器”中直接卸载此功能。

原因三:请求筛选规则拒绝了 POST

修复: IIS 管理器 → 选择你的网站 → 双击 “请求筛选” → “HTTP 谓词” 标签,检查是否有拒绝 POST 的条目。

原因四:处理程序映射未允许 POST 动词

修复: IIS 管理器 → “处理程序映射” → 找到对应的处理程序 → 右键编辑 → “请求限制” → “谓词” → 确保 POST 在允许列表中。

IIS 日志子状态码对照

405 错误后面的 sc-substatus 字段能精确定位:

  • 405.0 → HTTP 动词不被允许

  • 405.1 → 资源不支持该动词(通常是静态文件)

  • 405.2 → 请求筛选器拒绝


七、第六步:确认回调应答格式(关键!先应答,后处理业务)

微信支付回调通知有严格的响应超时限制(通常为 5秒),且没有重试时会累积延迟。如果你的代码在收到通知后先处理复杂的业务逻辑(更新订单、发消息、扣库存等),再返回应答,很容易超时导致微信判定失败。

正确做法:先验证签名并返回SUCCESS,再异步处理业务逻辑。

1. 返回格式要求

V3 接口(JSON 格式):

json

{"code": "SUCCESS", "message": "成功"}

关键要求:

  • code 必须是大写的 SUCCESS

  • Content-Type 必须是 application/json

  • 不能返回任何多余字符或 HTML

V2 接口(XML 格式):

xml

<xml>
  <return_code><![CDATA[SUCCESS]]></return_code>
  <return_msg><![CDATA[OK]]></return_msg>
</xml>

2. 推荐代码结构(V3 示例)

csharp

/// <summary>
/// 微信支付回调通知 - 正确姿势:先应答,后处理
/// </summary>
[HttpPost("notify")]
public async Task<IActionResult> Notify()
{
    // 读取请求体
    string body;
    using (var reader = new StreamReader(Request.Body))
    {
        body = await reader.ReadToEndAsync();
    }

    try
    {
        // 第一步:验证签名(必须同步完成)
        var isValid = await WechatPayHelper.VerifySignatureAsync(
            Request.Headers["Wechatpay-Serial"],
            Request.Headers["Wechatpay-Signature"],
            Request.Headers["Wechatpay-Timestamp"],
            Request.Headers["Wechatpay-Nonce"],
            body);

        if (!isValid)
        {
            // 签名验证失败,返回失败应答(微信会重试)
            return Ok(new { code = "FAIL", message = "签名验证失败" });
        }

        // 第二步:解析通知数据
        var notify = JsonSerializer.Deserialize<WechatPayNotify>(body);

        // 第三步:解密敏感数据
        var decryptedData = await WechatPayHelper.DecryptAsync(notify.Resource);

        // 第四步:先移除待查询队列(如果用了主动查询兜底)
        WechatPayQueryFallbackService.RemovePendingQuery(decryptedData.OutTradeNo);

        // 第五步:立即返回SUCCESS应答(微信收到后即认为通知成功)
        Response.ContentType = "application/json";

        // 第六步:异步处理业务逻辑(不阻塞当前响应)
        _ = Task.Run(async () =>
        {
            try
            {
                await ProcessOrderAsync(decryptedData);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "处理支付回调业务逻辑失败: {OutTradeNo}", decryptedData.OutTradeNo);
                // 异步处理失败时,记录到数据库,用定时任务补偿
                await RecordFailedOrderAsync(decryptedData.OutTradeNo);
            }
        });

        return Ok(new { code = "SUCCESS", message = "成功" });
    }
    catch (Exception ex)
    {
        _logger.LogError(ex, "处理微信支付通知异常");
        return Ok(new { code = "FAIL", message = "处理异常" });
    }
}

/// <summary>
/// 异步处理业务逻辑(更新订单状态、发送通知等)
/// </summary>
private async Task ProcessOrderAsync(WechatPayDecryptedData data)
{
    // 1. 更新订单状态(幂等操作,防止重复处理)
    await UpdateOrderStatusAsync(data.OutTradeNo, data.TransactionId);

    // 2. 发送消息通知
    await SendPaymentNotificationAsync(data.OutTradeNo);

    // 3. 扣库存等业务操作
    await DeductInventoryAsync(data.OutTradeNo);
}

3. 为什么必须这样做?

做法 结果
先更新订单 → 再返回SUCCESS 业务处理超过5秒 → 微信超时 → 判定失败 → 微信会重试
先返回SUCCESS → 再更新订单 微信立即收到成功应答 → 不重试 → 业务逻辑异步完成

4. 异步处理失败怎么办?

如果异步处理失败了(比如数据库挂了),微信不会再通知你。因此需要额外保障:

  • 异步处理必须有重试机制(失败时记录到数据库,定时任务重试)

  • 配合主动查询兜底,2分钟后主动查询支付状态,双重保障

5. 常见错误写法(不要这样写)

csharp

// ❌ 错误:先处理一大堆业务,最后才返回
[HttpPost("notify")]
public async Task<IActionResult> Notify()
{
    // ... 验证签名 ...
    
    // 耗时操作(微信已经等得不耐烦了)
    await UpdateOrderAsync();       // 可能很慢
    await SendSmsAsync();           // 可能很慢
    await DeductInventoryAsync();   // 可能很慢
    await SendAppPushAsync();       // 可能很慢
    
    // 这时候可能已经过了10秒,微信早就超时断开了
    return Ok(new { code = "SUCCESS", message = "成功" });
}

八、验证修复效果的方法

1. 创建测试订单验证(最直接)

  1. 创建一个金额 0.01 元的测试订单

  2. 用微信扫码支付

  3. 等待 1-2 分钟

  4. 检查 IIS 日志,看是否有回调的 POST 请求,状态码是否为 200

  5. 检查业务日志,看异步处理是否成功

2. 用 curl 从外网模拟测试

在你本地电脑或另一台外部服务器上执行:

bash

curl -X POST "https://你的回调域名/notify路径" \
  -H "Content-Type: application/json" \
  -d "{}" \
  -v

结果对照:

  • HTTP 200 → 服务连通性正常

  • Connection timed out → 安全组或防火墙拦截

  • SSL error → 证书或 TLS 版本问题

  • 405 Method Not Allowed → IIS 处理程序配置问题


九、主动查询兜底策略(生产必须)

在回调彻底修好之前,必须用主动查询兜底,确保不漏单。

策略:下单后超时 2 分钟未收到回调,主动查询一次

csharp

/// <summary>
/// 微信支付主动查询兜底服务
/// 策略:下单后 2 分钟未收到回调,主动查询微信支付状态
/// </summary>
public class WechatPayQueryFallbackService : BackgroundService
{
    private readonly IServiceScopeFactory _scopeFactory;
    private readonly ILogger<WechatPayQueryFallbackService> _logger;

    // 待查询队列:Key=商户订单号, Value=下单时间
    private static readonly ConcurrentDictionary<string, DateTime> _pendingOrders = new();

    public WechatPayQueryFallbackService(
        IServiceScopeFactory scopeFactory,
        ILogger<WechatPayQueryFallbackService> logger)
    {
        _scopeFactory = scopeFactory;
        _logger = logger;
    }

    /// <summary>
    /// 下单成功后调用,将订单加入查询队列
    /// </summary>
    public static void AddPendingQuery(string outTradeNo)
    {
        _pendingOrders.TryAdd(outTradeNo, DateTime.Now);
    }

    /// <summary>
    /// 收到回调后调用,从队列中移除
    /// </summary>
    public static void RemovePendingQuery(string outTradeNo)
    {
        _pendingOrders.TryRemove(outTradeNo, out _);
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                var now = DateTime.Now;
                var expiredOrders = _pendingOrders
                    .Where(o => (now - o.Value).TotalMinutes >= 2)
                    .Select(o => o.Key)
                    .ToList();

                foreach (var outTradeNo in expiredOrders)
                {
                    _pendingOrders.TryRemove(outTradeNo, out _);
                    await QueryOrderAsync(outTradeNo);
                }
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "主动查询微信支付状态异常");
            }

            // 每 30 秒检查一次
            await Task.Delay(TimeSpan.FromSeconds(30), stoppingToken);
        }
    }

    private async Task QueryOrderAsync(string outTradeNo)
    {
        using var scope = _scopeFactory.CreateScope();
        var wechatPayService = scope.ServiceProvider.GetRequiredService<IWechatPayService>();

        try
        {
            var result = await wechatPayService.QueryOrderByOutTradeNoAsync(outTradeNo);

            if (result.TradeState == "SUCCESS")
            {
                _logger.LogInformation("主动查询发现订单已支付: {OutTradeNo}", outTradeNo);
                // 更新本地订单状态为已支付
                await UpdateOrderStatusAsync(outTradeNo, result);
            }
            else
            {
                _logger.LogWarning("主动查询订单未支付: {OutTradeNo}, 状态: {TradeState}",
                    outTradeNo, result.TradeState);
                // 如果仍未支付,可以重新加入队列继续等待
                _pendingOrders.TryAdd(outTradeNo, DateTime.Now);
            }
        }
        catch (Exception ex)
        {
            _logger.LogError(ex, "查询微信支付订单失败: {OutTradeNo}", outTradeNo);
            // 查询失败时重新加入队列,避免因网络抖动漏单
            _pendingOrders.TryAdd(outTradeNo, DateTime.Now);
        }
    }

    private async Task UpdateOrderStatusAsync(string outTradeNo, object queryResult)
    {
        // TODO: 实现订单状态更新逻辑
        // 1. 更新数据库订单状态(幂等操作)
        // 2. 记录微信交易号
        // 3. 触发支付成功后的业务流程
    }
}

// ====== 使用示例 ======

// 下单接口中,下单成功后注册查询任务
[HttpPost("create-order")]
public async Task<IActionResult> CreateOrder()
{
    // ... 调用微信下单接口 ...
    var outTradeNo = Guid.NewGuid().ToString("N");

    // 下单成功后,加入2分钟后主动查询队列
    WechatPayQueryFallbackService.AddPendingQuery(outTradeNo);

    return Ok(new { outTradeNo });
}

// 回调接口中,收到通知后移除查询任务
[HttpPost("notify")]
public async Task<IActionResult> Notify()
{
    // ... 验证签名、解析数据 ...

    // 回调处理成功后,从待查询队列移除
    WechatPayQueryFallbackService.RemovePendingQuery(outTradeNo);

    // 先返回SUCCESS
    return Ok(new { code = "SUCCESS", message = "成功" });
}

主动查询接口(V3)

text

GET https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/{商户订单号}

只要返回 trade_state = SUCCESS,就更新本地订单为已支付。


排障检查清单

逐项检查,逐一排除:

  • 联系微信客服,拿到通知失败的具体错误原因

  • 用 MySSL.com 检测回调域名,确认 PCI DSS 合规

  • 确认 TLS 1.0 / TLS 1.1 已禁用

  • 确认阿里云 ECS 安全组 443 端口已放行

  • 检查 IIS 日志,确认是否有微信的 POST 请求记录

  • 如果有 405 错误,检查回调URL是否静态文件、WebDAV是否拦截、请求筛选是否拒绝POST

  • 确认回调代码先返回SUCCESS应答,再异步处理业务逻辑

  • 确认回调应答格式正确({"code":"SUCCESS","message":"成功"}

  • 确认 notify_url 以 https:// 开头,且不带参数

  • 用 curl 从外网模拟 POST 测试连通性

  • 配置主动查询兜底,超时 2 分钟主动调用查询接口

  • 异步处理业务逻辑有失败重试机制


总结

本次排障涉及了 TLS 协议配置、阿里云安全组、IIS 处理程序映射、回调应答格式、异步处理策略 五个层面。

最常见的三个坑:

  1. TLS 1.0/1.1 未禁用,导致 PCI DSS 不合规,微信拒绝握手

  2. 安全组没放行 443 端口,请求根本到不了 IIS

  3. IIS 返回 405,因为静态文件、WebDAV 或请求筛选拦截了 POST 请求

回调处理的核心原则:

  1. 先应答,后处理:5秒内必须返回SUCCESS,业务逻辑异步执行

  2. 必须有兜底:主动查询作为回调的补充,双重保障不丢单

  3. 失败要重试:异步处理失败时,需要记录并重试

排障最佳实践:

  • 先问微信客服拿失败原因,不要盲目排查

  • 每改一个配置,用新测试订单验证

  • 生产环境必须有主动查询兜底,绝不能只依赖回调


本文基于真实排障经历整理,希望对遇到同样问题的开发者有所帮助。

Logo

openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构

更多推荐