# 异步通知处理 - Node.js 示例

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

```javascript
const express = require("express");

// 支付宝异步通知是 application/x-www-form-urlencoded 表单，不能用 JSON parser 代替。
// createAlipayNotifyRouter 依赖项目里的真实 SDK 工厂、支付配置、订单仓储和通知事件仓储。
// 生产环境必须用持久化订单表和通知事件表做幂等；内存 Map 只能用于非生产 demo。
function createAlipayNotifyRouter(deps) {
  const router = express.Router();
  router.use(express.urlencoded({ extended: false }));

  router.post("/alipay/notify", async (req, res) => {
    const params = { ...req.body };

    try {
      const config = await deps.paymentConfigRepository.findByAppId(params.app_id);
      if (!config) {
        return fail(res, deps, "alipay notify app config not found", params);
      }

      const alipaySdk = deps.alipaySdkFactory.create(config);
      if (!(await verifyNotifyWithAlipaySdk(params, alipaySdk))) {
        return fail(res, deps, "alipay notify signature check failed", params);
      }

      const order = await deps.orderRepository.findByOutTradeNo(params.out_trade_no);
      if (!order || !businessFieldsMatch(order, params, config)) {
        return fail(res, deps, "alipay notify business check failed", params);
      }

      const inserted = await deps.notifyEventRepository.insertOnce({
        notifyId: params.notify_id,
        tradeNo: params.trade_no,
        outTradeNo: params.out_trade_no,
        tradeStatus: params.trade_status,
        rawParams: params,
      });
      if (!inserted) {
        return res.type("text/plain").send("success");
      }

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

      return res.type("text/plain").send("success");
    } catch (error) {
      safeLogNotifyError(deps, "alipay notify unexpected exception", params);
      return res.type("text/plain").send("fail");
    }
  });

  return router;
}

async function verifyNotifyWithAlipaySdk(params, alipaySdk) {
  // 落地前先查看目标项目 node_modules/alipay-sdk 的 package.json 和 .d.ts。
  // 如果当前版本类型定义确认存在 checkNotifySignV2(params)，按当前 SDK 实际 API 调用。
  if (typeof alipaySdk.checkNotifySignV2 !== "function") {
    throw new Error("current alipay-sdk does not expose checkNotifySignV2; confirm SDK types before coding");
  }
  return await Promise.resolve(alipaySdk.checkNotifySignV2(params));
}

function businessFieldsMatch(order, params, config) {
  return params.app_id === config.appId
    && params.out_trade_no === order.outTradeNo
    && amountEquals(order.totalAmount, params.total_amount)
    && expectedSellerMatches(config, params.seller_id, params.seller_email);
}

function isPaidTradeNotification(params) {
  const paidStatus = params.trade_status === "TRADE_SUCCESS" || params.trade_status === "TRADE_FINISHED";
  return paidStatus && !params.out_biz_no && !params.gmt_refund && !params.refund_fee;
}

function amountEquals(expected, actual) {
  const expectedAmount = normalizeAmountText(expected);
  const actualAmount = normalizeAmountText(actual);
  return expectedAmount !== null && expectedAmount === actualAmount;
}

// 返回“元”为单位、保留两位小数的金额文本，例如 88.00；不要用浮点数直接比较金额。
function normalizeAmountText(value) {
  if (value == null) return null;
  const text = String(value).trim();
  const match = text.match(/^(\d+)(?:\.(\d{1,2}))?$/);
  if (!match) return null;
  return `${BigInt(match[1]).toString()}.${(match[2] || "").padEnd(2, "0")}`;
}

function expectedSellerMatches(config, sellerId, sellerEmail) {
  const expectedSellerId = config.sellerId;
  const expectedSellerEmail = config.sellerEmail;
  return Boolean(
    (expectedSellerId && sellerId === expectedSellerId)
      || (expectedSellerEmail && sellerEmail === expectedSellerEmail)
  );
}

function fail(res, deps, message, params) {
  safeLogNotifyError(deps, message, params);
  return res.type("text/plain").send("fail");
}

function safeLogNotifyError(deps, message, params) {
  try {
    deps.logger.warn(message, sanitizeNotifyParams(params));
  } catch (error) {
    // 日志失败不能影响支付宝收到纯文本 fail。
  }
}

function sanitizeNotifyParams(params) {
  const { sign, ...safeParams } = params;
  return safeParams;
}

module.exports = { createAlipayNotifyRouter };
```

要点：

- `notify_url` 路由必须使用表单解析中间件，不要用 JSON parser 读取支付宝通知。
- 验签必须调用当前项目支付宝 SDK 的实际验签 API；Node.js 落地前先查目标项目 `alipay-sdk` 类型定义确认 `checkNotifySignV2(params)` 是否存在。
- `app_id`、`seller_id` / `seller_email` 从运行时支付配置校验，不从订单对象默认取值。
- 金额校验使用“元”为单位的两位小数字符串或项目定点数工具，不要用浮点数直接比较。
- 只有付款成功状态且不是退款、关单、分账等事件时，才认定付款成功。
- 内存订单或内存幂等只能用于非生产 demo，生产必须使用持久化订单表和通知事件表。
- 处理成功后返回纯文本 `success`；异常、验签失败或业务校验失败返回 `fail`。
