# 异步通知处理 - C# 示例

适用产品：AI 网页应用收款、AI 移动应用收款。AI 按量付费不使用异步通知。

```csharp
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;

[ApiController]
public class AlipayNotifyController : ControllerBase
{
    private readonly IAlipayNotifyVerifier _alipayNotifyVerifier;
    private readonly IPaymentConfigRepository _paymentConfigRepository;
    private readonly IOrderRepository _orderRepository;
    private readonly INotifyEventRepository _notifyEventRepository;
    private readonly INotifyLogger _notifyLogger;

    public AlipayNotifyController(
        IAlipayNotifyVerifier alipayNotifyVerifier,
        IPaymentConfigRepository paymentConfigRepository,
        IOrderRepository orderRepository,
        INotifyEventRepository notifyEventRepository,
        INotifyLogger notifyLogger)
    {
        _alipayNotifyVerifier = alipayNotifyVerifier;
        _paymentConfigRepository = paymentConfigRepository;
        _orderRepository = orderRepository;
        _notifyEventRepository = notifyEventRepository;
        _notifyLogger = notifyLogger;
    }

    [HttpPost("/alipay/notify")]
    public async Task<IActionResult> Notify()
    {
        var paramsMap = new Dictionary<string, string>();
        try
        {
            paramsMap = Request.Form.ToDictionary(kv => kv.Key, kv => kv.Value.ToString());
            var config = await _paymentConfigRepository.FindByAppId(paramsMap.GetValueOrDefault("app_id"));
            if (config == null)
            {
                return Fail("alipay notify app config not found", paramsMap);
            }

            if (!VerifyNotifyWithAlipaySdk(paramsMap, config))
            {
                return Fail("alipay notify signature check failed", paramsMap);
            }

            var order = await _orderRepository.FindByOutTradeNo(paramsMap.GetValueOrDefault("out_trade_no"));
            if (order == null || !BusinessFieldsMatch(order, paramsMap, config))
            {
                return Fail("alipay notify business check failed", paramsMap);
            }

            var inserted = await _notifyEventRepository.InsertOnce(
                paramsMap.GetValueOrDefault("notify_id"),
                paramsMap.GetValueOrDefault("trade_no"),
                paramsMap.GetValueOrDefault("out_trade_no"),
                paramsMap.GetValueOrDefault("trade_status"),
                paramsMap);
            if (!inserted)
            {
                return Content("success", "text/plain");
            }

            if (IsPaidTradeNotification(paramsMap))
            {
                await _orderRepository.MarkPaidIdempotently(order.OutTradeNo, paramsMap.GetValueOrDefault("trade_no"), paramsMap);
            }
            else
            {
                await _orderRepository.RecordNonPaidTradeEvent(order.OutTradeNo, paramsMap.GetValueOrDefault("trade_status"), paramsMap);
            }

            return Content("success", "text/plain");
        }
        catch (Exception)
        {
            SafeLogNotifyError("alipay notify unexpected exception", paramsMap);
            return Content("fail", "text/plain");
        }
    }

    private bool BusinessFieldsMatch(MerchantOrder order, Dictionary<string, string> paramsMap, AlipayNotifyConfig config)
    {
        return paramsMap.GetValueOrDefault("app_id") == config.AppId
            && paramsMap.GetValueOrDefault("out_trade_no") == order.OutTradeNo
            && AmountEquals(order.TotalAmount, paramsMap.GetValueOrDefault("total_amount"))
            && ExpectedSellerMatches(config, paramsMap.GetValueOrDefault("seller_id"), paramsMap.GetValueOrDefault("seller_email"));
    }

    private static bool AmountEquals(decimal expected, string? actual)
    {
        var expectedAmount = NormalizeAmountText(expected);
        var actualAmount = NormalizeAmountText(actual);
        return expectedAmount != null && expectedAmount == actualAmount;
    }

    private static string? NormalizeAmountText(decimal value)
    {
        if (decimal.Round(value, 2) != value) return null;
        return value.ToString("0.00", CultureInfo.InvariantCulture);
    }

    private static string? NormalizeAmountText(string? value)
    {
        if (string.IsNullOrWhiteSpace(value)) return null;
        var match = Regex.Match(value.Trim(), @"^(\d+)(?:\.(\d{1,2}))?$");
        if (!match.Success) return null;
        var yuan = match.Groups[1].Value.TrimStart('0');
        if (yuan.Length == 0) yuan = "0";
        return $"{yuan}.{match.Groups[2].Value.PadRight(2, '0')}";
    }

    private static bool IsPaidTradeNotification(Dictionary<string, string> paramsMap)
    {
        var paidStatus = paramsMap.GetValueOrDefault("trade_status") == "TRADE_SUCCESS"
            || paramsMap.GetValueOrDefault("trade_status") == "TRADE_FINISHED";
        return paidStatus
            && string.IsNullOrEmpty(paramsMap.GetValueOrDefault("out_biz_no"))
            && string.IsNullOrEmpty(paramsMap.GetValueOrDefault("gmt_refund"))
            && string.IsNullOrEmpty(paramsMap.GetValueOrDefault("refund_fee"));
    }

    private static bool ExpectedSellerMatches(AlipayNotifyConfig config, string? sellerId, string? sellerEmail)
    {
        var expectedSellerId = config.SellerId;
        var expectedSellerEmail = config.SellerEmail;
        return (!string.IsNullOrEmpty(expectedSellerId) && sellerId == expectedSellerId)
            || (!string.IsNullOrEmpty(expectedSellerEmail) && sellerEmail == expectedSellerEmail);
    }

    private bool VerifyNotifyWithAlipaySdk(Dictionary<string, string> paramsMap, AlipayNotifyConfig config)
    {
        // 使用当前项目已安装的支付宝 SDK 验签能力校验 paramsMap["sign"]。
        // _alipayNotifyVerifier.Verify(paramsMap, config) 必须封装目标项目当前 SDK 实际通知验签 API；落地前先查已安装包文档或类型定义，不能手写临时 RSA 验签。
        return _alipayNotifyVerifier.Verify(paramsMap, config);
    }

    private IActionResult Fail(string message, Dictionary<string, string> paramsMap)
    {
        SafeLogNotifyError(message, paramsMap);
        return Content("fail", "text/plain");
    }

    private void SafeLogNotifyError(string message, Dictionary<string, string> paramsMap)
    {
        try
        {
            _notifyLogger.Warn(message, SanitizeNotifyParams(paramsMap));
        }
        catch (Exception)
        {
            // 日志失败不能影响支付宝收到纯文本 fail。
        }
    }

    private static Dictionary<string, string> SanitizeNotifyParams(Dictionary<string, string> paramsMap)
    {
        var safeParams = new Dictionary<string, string>(paramsMap);
        safeParams.Remove("sign");
        return safeParams;
    }
}

// 以下接口对接商家项目的真实 SDK 验签、支付配置、订单仓储、通知事件表和脱敏日志。
// 生产环境必须使用持久化订单表和通知事件表；内存 Dictionary 只能用于非生产 demo。
public interface IAlipayNotifyVerifier { bool Verify(Dictionary<string, string> paramsMap, AlipayNotifyConfig config); }
public interface IPaymentConfigRepository { Task<AlipayNotifyConfig?> FindByAppId(string? appId); }
public interface IOrderRepository
{
    Task<MerchantOrder?> FindByOutTradeNo(string? outTradeNo);
    Task MarkPaidIdempotently(string outTradeNo, string? tradeNo, Dictionary<string, string> paramsMap);
    Task RecordNonPaidTradeEvent(string outTradeNo, string? tradeStatus, Dictionary<string, string> paramsMap);
}
public interface INotifyEventRepository
{
    Task<bool> InsertOnce(string? notifyId, string? tradeNo, string? outTradeNo, string? tradeStatus, Dictionary<string, string> paramsMap);
}
public interface INotifyLogger { void Warn(string message, Dictionary<string, string> paramsMap); }
public record AlipayNotifyConfig(string AppId, string AlipayPublicKey, string? SellerId, string? SellerEmail);
public record MerchantOrder(string OutTradeNo, decimal TotalAmount);
```

要点：

- 支付宝通知是 POST 表单，验签前必须保留完整表单参数。
- 验签必须调用当前项目支付宝 SDK 的实际验签 API，验签前不得丢字段或改值。
- `app_id`、`seller_id` / `seller_email` 从运行时支付配置校验，不从订单对象默认取值。
- 金额校验使用 `decimal` 或项目定点数工具，不要用浮点数直接比较。
- 只有付款成功状态且不是退款、关单、分账等事件时，才认定付款成功。
- 内存订单或内存幂等只能用于非生产 demo，生产必须使用持久化订单表和通知事件表。
- 处理成功后返回纯文本 `success`；异常、验签失败或业务校验失败返回 `fail`。
