# 异步通知处理 - Java 示例

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

```java
import com.alipay.api.internal.util.AlipaySignature;
import jakarta.servlet.http.HttpServletRequest;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseBody;

import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;

public class AlipayNotifyController {
    private static final String CHARSET = "UTF-8";
    private final PaymentConfigRepository paymentConfigRepository;
    private final OrderRepository orderRepository;
    private final NotifyEventRepository notifyEventRepository;
    private final NotifyLogger notifyLogger;

    public AlipayNotifyController(
        PaymentConfigRepository paymentConfigRepository,
        OrderRepository orderRepository,
        NotifyEventRepository notifyEventRepository,
        NotifyLogger notifyLogger
    ) {
        this.paymentConfigRepository = paymentConfigRepository;
        this.orderRepository = orderRepository;
        this.notifyEventRepository = notifyEventRepository;
        this.notifyLogger = notifyLogger;
    }

    @PostMapping(value = "/alipay/notify", produces = "text/plain;charset=UTF-8")
    @ResponseBody
    public String handleAlipayNotify(HttpServletRequest request) {
        Map<String, String> params = new HashMap<>();
        try {
            params = collectFormParams(request);
            AlipayNotifyConfig config = paymentConfigRepository.findByAppId(params.get("app_id"));

            if (config == null || !verifyNotifyWithAlipaySdk(params, config)) {
                return fail("alipay notify signature check failed", params);
            }

            MerchantOrder order = orderRepository.findByOutTradeNo(params.get("out_trade_no"));
            if (order == null || !businessFieldsMatch(order, params, config)) {
                return fail("alipay notify business check failed", params);
            }

            boolean inserted = notifyEventRepository.insertOnce(
                params.get("notify_id"),
                params.get("trade_no"),
                params.get("out_trade_no"),
                params.get("trade_status"),
                params
            );
            if (!inserted) {
                return "success";
            }

            if (isPaidTradeNotification(params)) {
                orderRepository.markPaidIdempotently(order.outTradeNo(), params.get("trade_no"), params);
            } else {
                orderRepository.recordNonPaidTradeEvent(order.outTradeNo(), params.get("trade_status"), params);
            }

            return "success";
        } catch (Exception e) {
            safeLogNotifyError("alipay notify unexpected exception", params);
            return "fail";
        }
    }

    private static Map<String, String> collectFormParams(HttpServletRequest request) {
        Map<String, String> params = new HashMap<>();
        request.getParameterMap().forEach((name, values) -> {
            String value = String.join(",", values);
            params.put(name, value);
        });
        return params;
    }

    private static boolean verifyNotifyWithAlipaySdk(Map<String, String> params, AlipayNotifyConfig config) throws Exception {
        return AlipaySignature.rsaCheckV1(
            params,
            config.alipayPublicKey(),
            CHARSET,
            params.getOrDefault("sign_type", "RSA2")
        );
    }

    private static boolean businessFieldsMatch(MerchantOrder order, Map<String, String> params, AlipayNotifyConfig config) {
        return config.appId().equals(params.get("app_id"))
            && order.outTradeNo().equals(params.get("out_trade_no"))
            && amountEquals(order.totalAmount(), params.get("total_amount"))
            && expectedSellerMatches(config, params.get("seller_id"), params.get("seller_email"));
    }

    private static boolean amountEquals(BigDecimal expected, String actual) {
        if (actual == null) return false;
        try {
            return expected.compareTo(new BigDecimal(actual)) == 0;
        } catch (NumberFormatException e) {
            return false;
        }
    }

    private static boolean isPaidTradeNotification(Map<String, String> params) {
        boolean paidStatus = "TRADE_SUCCESS".equals(params.get("trade_status")) || "TRADE_FINISHED".equals(params.get("trade_status"));
        return paidStatus
            && isBlank(params.get("out_biz_no"))
            && isBlank(params.get("gmt_refund"))
            && isBlank(params.get("refund_fee"));
    }

    private static boolean expectedSellerMatches(AlipayNotifyConfig config, String sellerId, String sellerEmail) {
        String expectedSellerId = blankToNull(config.sellerId());
        String expectedSellerEmail = blankToNull(config.sellerEmail());
        return (expectedSellerId != null && expectedSellerId.equals(sellerId))
            || (expectedSellerEmail != null && expectedSellerEmail.equals(sellerEmail));
    }

    private static boolean isBlank(String value) {
        return value == null || value.isBlank();
    }

    private static String blankToNull(String value) {
        return isBlank(value) ? null : value;
    }

    private String fail(String message, Map<String, String> params) {
        safeLogNotifyError(message, params);
        return "fail";
    }

    private void safeLogNotifyError(String message, Map<String, String> params) {
        try {
            notifyLogger.warn(message, sanitizeNotifyParams(params));
        } catch (Exception ignored) {
            // 日志失败不能影响支付宝收到纯文本 fail。
        }
    }

    private static Map<String, String> sanitizeNotifyParams(Map<String, String> params) {
        Map<String, String> safeParams = new HashMap<>(params);
        safeParams.remove("sign");
        return safeParams;
    }

    // 以下接口对接商家项目的真实配置、订单仓储、通知事件表和脱敏日志。
    // 生产环境必须使用持久化订单表和通知事件表；内存 Map 只能用于非生产 demo。
    private interface PaymentConfigRepository {
        AlipayNotifyConfig findByAppId(String appId);
    }

    private interface OrderRepository {
        MerchantOrder findByOutTradeNo(String outTradeNo);
        void markPaidIdempotently(String outTradeNo, String tradeNo, Map<String, String> params);
        void recordNonPaidTradeEvent(String outTradeNo, String tradeStatus, Map<String, String> params);
    }

    private interface NotifyEventRepository {
        boolean insertOnce(String notifyId, String tradeNo, String outTradeNo, String tradeStatus, Map<String, String> params);
    }

    private interface NotifyLogger {
        void warn(String message, Map<String, String> params);
    }

    private record AlipayNotifyConfig(String appId, String alipayPublicKey, String sellerId, String sellerEmail) {}
    private record MerchantOrder(String outTradeNo, BigDecimal totalAmount) {}
}
```

要点：

- 支付宝通知是 POST 表单，验签前必须收集完整参数 map。
- `AlipaySignature.rsaCheckV1` 会忽略 `sign` / `sign_type` 完成验签，业务代码不要先删除或改写参数。
- `app_id`、`seller_id` / `seller_email` 从运行时支付配置校验，不从订单对象默认取值。
- 金额校验使用 `BigDecimal` 或项目定点数工具，不要用浮点数直接比较。
- 只有付款成功状态且不是退款、关单、分账等事件时，才认定付款成功。
- 内存订单或内存幂等只能用于非生产 demo，生产必须使用持久化订单表和通知事件表。
- 处理成功后返回纯文本 `success`；异常、验签失败或业务校验失败返回 `fail`。
