01MVP 标识01MVP
包文档基础设施服务payment 支付服务

payment 支付服务

统一的支付集成包,支持 Stripe 和微信支付

本页面包含支付配置和 API 使用的完整指南。

概述

@mono/payment 提供统一的支付服务接口,支持多个支付提供商。无论使用哪个提供商,都可以通过相同的 API 处理支付流程。

核心功能

  • 支持 Stripe(国际支付)和微信支付(中国市场)
  • 统一的 API 接口,轻松切换提供商
  • 微信支付三种渠道:NATIVE(扫码)、JSAPI(公众号)、MINIPROGRAM_BRIDGE(小程序)
  • Webhook 签名验证
  • 订单查询和管理
  • 完善的错误处理和日志记录

快速开始

安装

pnpm add @mono/payment

Stripe 基础用法

import { createPaymentProvider } from "@mono/payment";

// 创建 Stripe 提供商
const stripeProvider = createPaymentProvider("stripe");

// 创建支付
const result = await stripeProvider.createPayment({
  orderId: "order_123",
  userId: "user_123",
  planId: "monthly",
  amount: 10,
  currency: "usd",
});

// 跳转到支付页面
window.location.href = result.paymentUrl;

微信支付基础用法

import { createWechatPayProvider, type WechatPaymentParams } from "@mono/payment";

// 从环境变量创建提供商
const wechatProvider = createWechatPayProvider();

// 创建 NATIVE 支付(扫码)
const params: WechatPaymentParams = {
  orderId: "order_123",
  userId: "user_123",
  planId: "monthly",
  amount: 99.99,
  currency: "CNY",
  channel: { type: "NATIVE" },
  description: "月度订阅",
};

const result = await wechatProvider.createPayment(params);

// 显示二维码给用户扫描
console.log("二维码链接:", result.codeUrl);

微信支付渠道选择

微信支付支持三种支付渠道,根据用户环境自动选择:

渠道使用场景必需参数返回内容
NATIVEPC 网站扫码支付-codeUrl(二维码链接)
JSAPI微信公众号内支付openidjsapiParams(支付参数)
MINIPROGRAM_BRIDGE小程序支付openidjsapiParams(支付参数)

自动渠道选择

function selectWechatPayChannel(userAgent: string, openid?: string) {
  // 小程序环境
  if (userAgent.includes("miniProgram")) {
    return { type: "MINIPROGRAM_BRIDGE", openid };
  }

  // 微信浏览器(移动端)
  if (userAgent.includes("MicroMessenger") && openid) {
    return { type: "JSAPI", openid };
  }

  // PC 或其他浏览器
  return { type: "NATIVE" };
}

实战示例

示例 1:NATIVE 扫码支付(PC)

// app/api/payment/wechat/native/route.ts
import { createWechatPayProvider, type WechatPaymentParams } from "@mono/payment";
import { NextResponse } from "next/server";

export async function POST(req: Request) {
  try {
    const { orderId, amount, userId } = await req.json();

    const wechatProvider = createWechatPayProvider();
    const params: WechatPaymentParams = {
      orderId,
      userId,
      planId: "monthly",
      amount,
      currency: "CNY",
      channel: { type: "NATIVE" },
      description: "月度订阅",
    };

    const result = await wechatProvider.createPayment(params);

    return NextResponse.json({
      success: true,
      codeUrl: result.codeUrl,
      orderId: result.providerOrderId,
    });
  } catch (error) {
    console.error("创建支付失败:", error);
    return NextResponse.json(
      { error: "支付创建失败" },
      { status: 500 }
    );
  }
}

前端展示二维码:

// components/WechatPayQRCode.tsx
import { QRCodeSVG } from "qrcode.react";
import { useState, useEffect } from "react";

export function WechatPayQRCode({ orderId, amount }: { orderId: string; amount: number }) {
  const [codeUrl, setCodeUrl] = useState<string>("");
  const [status, setStatus] = useState<"pending" | "paid" | "failed">("pending");

  useEffect(() => {
    // 创建支付
    fetch("/api/payment/wechat/native", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ orderId, amount, userId: "current-user-id" }),
    })
      .then((res) => res.json())
      .then((data) => setCodeUrl(data.codeUrl));

    // 轮询订单状态
    const interval = setInterval(async () => {
      const res = await fetch(`/api/payment/status/${orderId}`);
      const data = await res.json();

      if (data.status === "paid") {
        setStatus("paid");
        clearInterval(interval);
      }
    }, 2000);

    return () => clearInterval(interval);
  }, [orderId, amount]);

  if (status === "paid") {
    return <div className="text-green-600">支付成功!</div>;
  }

  return (
    <div className="flex flex-col items-center gap-4">
      <h3 className="text-lg font-semibold">请使用微信扫码支付</h3>
      {codeUrl && <QRCodeSVG value={codeUrl} size={256} />}
      <p className="text-sm text-muted-foreground">
        支付金额: ¥{amount.toFixed(2)}
      </p>
    </div>
  );
}

示例 2:JSAPI 公众号支付

// app/api/payment/wechat/jsapi/route.ts
import { createWechatPayProvider, type WechatPaymentParams } from "@mono/payment";
import { NextResponse } from "next/server";

export async function POST(req: Request) {
  try {
    const { orderId, amount, userId, openid } = await req.json();

    if (!openid) {
      return NextResponse.json(
        { error: "需要用户 openid" },
        { status: 400 }
      );
    }

    const wechatProvider = createWechatPayProvider();
    const params: WechatPaymentParams = {
      orderId,
      userId,
      planId: "monthly",
      amount,
      currency: "CNY",
      channel: {
        type: "JSAPI",
        openid, // 通过微信 OAuth 获取
      },
      description: "月度订阅",
    };

    const result = await wechatProvider.createPayment(params);

    return NextResponse.json({
      success: true,
      jsapiParams: result.jsapiParams,
      orderId: result.providerOrderId,
    });
  } catch (error) {
    console.error("创建支付失败:", error);
    return NextResponse.json(
      { error: "支付创建失败" },
      { status: 500 }
    );
  }
}

前端调用微信 JSAPI:

// components/WechatJSAPIPay.tsx
"use client";

import { useState } from "react";

declare global {
  interface Window {
    WeixinJSBridge: any;
  }
}

export function WechatJSAPIPay({ orderId, amount, openid }: {
  orderId: string;
  amount: number;
  openid: string;
}) {
  const [paying, setPaying] = useState(false);

  const handlePay = async () => {
    setPaying(true);

    try {
      // 创建支付
      const res = await fetch("/api/payment/wechat/jsapi", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ orderId, amount, userId: "current-user-id", openid }),
      });

      const data = await res.json();

      if (!data.success) {
        throw new Error(data.error);
      }

      // 调用微信支付
      if (typeof window.WeixinJSBridge === "undefined") {
        alert("请在微信中打开");
        return;
      }

      window.WeixinJSBridge.invoke(
        "getBrandWCPayRequest",
        data.jsapiParams,
        (res: any) => {
          if (res.err_msg === "get_brand_wcpay_request:ok") {
            // 支付成功
            window.location.href = "/payment-success";
          } else {
            // 支付失败或取消
            alert("支付失败");
          }
          setPaying(false);
        }
      );
    } catch (error) {
      console.error("支付错误:", error);
      alert("支付失败,请重试");
      setPaying(false);
    }
  };

  return (
    <button
      onClick={handlePay}
      disabled={paying}
      className="w-full bg-green-600 text-white py-3 rounded-lg disabled:opacity-50"
    >
      {paying ? "处理中..." : `微信支付 ¥${amount.toFixed(2)}`}
    </button>
  );
}

示例 3:小程序支付

// 小程序端代码
// pages/payment/index.ts
Page({
  data: {
    orderId: "",
    amount: 0,
  },

  async onPay() {
    try {
      // 调用后端创建支付
      const res = await wx.request({
        url: "https://yourdomain.com/api/payment/wechat/miniprogram",
        method: "POST",
        data: {
          orderId: this.data.orderId,
          amount: this.data.amount,
          userId: "current-user-id",
          openid: wx.getStorageSync("openid"),
        },
      });

      if (!res.data.success) {
        throw new Error(res.data.error);
      }

      // 调用小程序支付
      const payResult = await wx.requestPayment(res.data.jsapiParams);

      // 支付成功
      wx.showToast({
        title: "支付成功",
        icon: "success",
      });

      // 跳转到成功页面
      wx.redirectTo({
        url: "/pages/payment-success/index",
      });
    } catch (error) {
      console.error("支付失败:", error);
      wx.showToast({
        title: "支付失败",
        icon: "error",
      });
    }
  },
});

示例 4:Webhook 处理

// app/api/webhooks/wechat-pay/route.ts
import { createWechatPayProvider } from "@mono/payment";
import { NextResponse } from "next/server";
import { db } from "@/lib/db";

export async function POST(req: Request) {
  try {
    const body = await req.text();
    const signature = req.headers.get("wechatpay-signature") || "";

    const wechatProvider = createWechatPayProvider();
    const verification = await wechatProvider.handleWebhook(body, signature);

    if (verification.success && verification.orderId) {
      // 更新数据库订单状态
      await db.order.update({
        where: { id: verification.orderId },
        data: {
          status: "paid",
          paidAt: new Date(),
        },
      });

      // 发送支付成功通知
      await sendPaymentSuccessNotification(verification.orderId);

      // 返回成功响应(必须)
      return NextResponse.json({
        code: "SUCCESS",
        message: "成功",
      });
    }

    return NextResponse.json(
      { code: "FAIL", message: "验证失败" },
      { status: 400 }
    );
  } catch (error) {
    console.error("Webhook 处理错误:", error);
    return NextResponse.json(
      { code: "FAIL", message: "处理失败" },
      { status: 500 }
    );
  }
}

示例 5:订单状态查询

// app/api/payment/status/[orderId]/route.ts
import { createWechatPayProvider } from "@mono/payment";
import { NextResponse } from "next/server";

export async function GET(
  req: Request,
  { params }: { params: { orderId: string } }
) {
  try {
    const wechatProvider = createWechatPayProvider();
    const orderStatus = await wechatProvider.queryOrder(params.orderId);

    return NextResponse.json({
      orderId: params.orderId,
      status: orderStatus.status,
      metadata: orderStatus.metadata,
    });
  } catch (error) {
    console.error("查询订单失败:", error);
    return NextResponse.json(
      { error: "查询失败" },
      { status: 500 }
    );
  }
}

示例 6:订单超时关闭

// lib/payment/order-timeout.ts
import { createWechatPayProvider } from "@mono/payment";
import { db } from "@/lib/db";

export async function closeExpiredOrders() {
  // 查找超过 30 分钟未支付的订单
  const expiredOrders = await db.order.findMany({
    where: {
      status: "pending",
      createdAt: {
        lt: new Date(Date.now() - 30 * 60 * 1000),
      },
    },
  });

  const wechatProvider = createWechatPayProvider();

  for (const order of expiredOrders) {
    try {
      // 关闭微信支付订单
      const success = await wechatProvider.closeOrder(order.id);

      if (success) {
        // 更新数据库状态
        await db.order.update({
          where: { id: order.id },
          data: { status: "closed" },
        });

        console.log(`订单 ${order.id} 已关闭`);
      }
    } catch (error) {
      console.error(`关闭订单 ${order.id} 失败:`, error);
    }
  }
}

// 使用 cron job 定期执行
// 例如:每 10 分钟执行一次

环境变量配置

Stripe

STRIPE_SECRET_KEY=sk_test_xxxxxxxx
STRIPE_WEBHOOK_SECRET=whsec_xxxxxxxx
NEXT_PUBLIC_APP_URL=https://yourdomain.com

微信支付

# 微信支付配置
WECHAT_PAY_APP_ID=wx1234567890abcdef          # 微信公众号/小程序 AppID
WECHAT_PAY_MCH_ID=1234567890                  # 商户号
WECHAT_PAY_API_V3_KEY=your-32-character-key   # API v3 密钥 (32字符)
WECHAT_PAY_SERIAL_NO=1234567890ABCDEF         # 证书序列号
WECHAT_PAY_PRIVATE_KEY="-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC7VJTUt9Us8cKj
...
-----END PRIVATE KEY-----"                     # 商户私钥 (完整 PEM 格式)
WECHAT_PAY_NOTIFY_URL=https://yourdomain.com/api/webhooks/wechat-pay  # 支付回调 URL (可选)

如何获取微信支付凭证:

  1. AppID: 从微信公众平台小程序平台获取
  2. 商户号 (mchId): 从微信支付商户平台获取
  3. API v3 密钥: 在商户平台 → 账户中心 → API 安全 → 设置 API 密钥(32 个字符)
  4. 证书序列号: 从商户平台 → 账户中心 → API 安全 → 管理证书 下载证书
  5. 商户私钥: 使用微信支付证书工具生成,妥善保管

API 参考

createPaymentProvider

创建支付提供商实例。

function createPaymentProvider(
  provider: "stripe" | "wechat"
): PaymentProvider;

createWechatPayProvider

创建微信支付提供商实例。

function createWechatPayProvider(
  config?: WechatPayConfig
): WechatPayProvider;

PaymentProvider.createPayment

创建支付订单。

async createPayment(params: PaymentParams): Promise<PaymentResult>;

PaymentProvider.handleWebhook

处理支付回调。

async handleWebhook(
  payload: string | Record<string, unknown>,
  signature: string
): Promise<WebhookVerification>;

WechatPayProvider.queryOrder

查询订单状态。

async queryOrder(orderId: string): Promise<{
  status: "pending" | "paid" | "failed";
  metadata?: Record<string, unknown>;
}>;

WechatPayProvider.closeOrder

关闭未支付订单。

async closeOrder(orderId: string): Promise<boolean>;

最佳实践

1. 安全性

  • 保护私钥: 存储在环境变量中,永远不要提交到 git
  • 验证 Webhook: 始终验证签名后再处理
  • 使用 HTTPS: 生产环境所有支付 URL 必须使用 HTTPS
  • 实现幂等性: 处理重复的 webhook 通知

2. 错误处理

try {
  const result = await provider.createPayment(params);
  // 处理成功
} catch (error) {
  // 记录错误用于调试
  console.error("支付创建失败:", error);

  // 返回用户友好的错误
  return { error: "无法处理支付,请重试" };
}

3. 订单管理

  • 生成唯一订单 ID(例如:order_${Date.now()}_${randomString()}
  • 在数据库中存储订单状态
  • 实现订单超时机制
  • 关闭过期的未支付订单

4. 测试

  • 开发环境使用沙箱/测试环境
  • 测试所有支付渠道
  • 使用模拟数据测试 webhook 处理
  • 验证签名验证逻辑

5. 监控

  • 记录所有支付操作
  • 监控 webhook 送达情况
  • 跟踪支付成功率
  • 设置失败告警

故障排查

微信支付问题

问题:"Missing required environment variable"

  • 确保所有微信支付环境变量已设置
  • 检查变量名称完全匹配
  • 添加变量后重启服务器

问题:"Signature verification failed"

  • 验证证书序列号是否正确
  • 检查私钥格式(必须是 PEM 格式)
  • 确保 API v3 密钥正好 32 个字符

问题:"OpenID is required for JSAPI payment"

  • JSAPI 和 MINIPROGRAM_BRIDGE 渠道需要用户的 openid
  • 通过微信 OAuth 流程获取 openid
  • 在 channel 配置中传入 openid

问题:Webhook 未收到通知

  • 验证 webhook URL 可公开访问(HTTPS)
  • 检查防火墙/安全组设置
  • 使用微信支付沙箱测试 webhook URL
  • 确保 webhook 返回正确的响应格式

Stripe 问题

问题:"No such customer"

  • 验证 Stripe 密钥是否正确
  • 检查是否一致使用测试/生产模式

问题:Webhook 签名不匹配

  • 验证 webhook 密钥与 Stripe 控制台匹配
  • 确保使用原始请求体(不是解析后的 JSON)

相关文档

完整的 API 文档请参考 README.md