You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何通过Stripe API实现关联客户信息的教练月度Payout邮件推送

解决方案:通过Stripe API关联Payout与客户信息并发送教练结算邮件

要实现将Stripe Payout ID与对应客户信息关联并发送给教练,核心是通过Stripe API追溯Payout中的资金来源,结合你平台的业务数据生成结算明细,具体步骤如下:

1. 监听Payout完成的Webhook事件

当Stripe向教练的Connected账户完成结算(Payout)时,会触发payout.paid事件。你需要在Stripe Dashboard中配置Webhook,接收该事件——这是触发邮件通知的最佳时机,确保只在资金实际到账后发送。

注意:必须验证Webhook请求的签名,防止伪造请求。Stripe SDK提供了现成的验证方法,不要跳过这一步。

2. 获取Payout对应的所有交易记录

每个Payout由多笔交易(Transaction)组成,你需要调用payouts.listTransactions接口,传入Payout ID并指定教练的Connected账户ID(通过stripeAccount参数),获取该Payout下的所有交易。

过滤出type为transfer的交易——这些交易是平台向教练转账的记录,对应客户的订阅服务费。

3. 追溯交易对应的客户与订阅信息

对于每笔transfer类型的交易:

  • 调用transfers.retrieve接口获取对应的Transfer对象,从source_transaction字段拿到原始的Charge ID(客户订阅产生的扣款记录)。
  • 调用charges.retrieve接口获取Charge对象,从中取出customer字段(Stripe客户ID)。
  • 从你自己的数据库中查询该Charge/Stripe客户ID对应的教练信息、客户姓名/邮箱、订阅周期等业务数据——这一步依赖你平台的业务存储,因为Stripe不会保存你平台的客户-教练匹配关系。

4. 生成邮件并发送

将上述整理好的Payout ID、客户列表、对应金额、服务周期等信息生成邮件内容,用你常用的邮件服务(如Nodemailer、SendGrid等)发送给教练。

代码示例(Node.js)

const stripe = require('stripe')('你的平台Secret Key');
const nodemailer = require('nodemailer'); // 以Nodemailer为例,替换为你的邮件服务

// 初始化邮件客户端
const emailTransporter = nodemailer.createTransport({
  // 配置你的邮件服务,比如SMTP或第三方服务
  host: 'smtp.example.com',
  port: 587,
  auth: {
    user: 'your-email@example.com',
    pass: 'your-email-password'
  }
});

// 处理Stripe Webhook
app.post('/stripe-webhook', async (req, res) => {
  const sig = req.headers['stripe-signature'];
  const webhookSecret = '你的Webhook Secret';
  
  let event;
  try {
    event = stripe.webhooks.constructEvent(req.body, sig, webhookSecret);
  } catch (err) {
    return res.status(400).send(`Webhook Error: ${err.message}`);
  }

  if (event.type === 'payout.paid') {
    const payout = event.data.object;
    const connectedAccountId = payout.account;
    // 从你的数据库中根据Connected账户ID获取教练的邮箱
    const coach = await yourDatabase.query(
      'SELECT email, name FROM coaches WHERE stripe_connected_account_id = $1',
      [connectedAccountId]
    );
    if (!coach) return res.json({ received: true });

    // 获取Payout所有交易(处理分页)
    let allTransactions = [];
    let hasMore = true;
    let startingAfter = null;

    while (hasMore) {
      const transactions = await stripe.payouts.listTransactions(
        payout.id,
        { limit: 100, starting_after: startingAfter },
        { stripeAccount: connectedAccountId }
      );
      allTransactions = [...allTransactions, ...transactions.data];
      hasMore = transactions.has_more;
      startingAfter = transactions.data[transactions.data.length - 1]?.id;
    }

    // 过滤出Transfer类型的交易
    const transferTransactions = allTransactions.filter(tx => tx.type === 'transfer');
    const payoutDetails = [];

    for (const tx of transferTransactions) {
      // 获取Transfer对象
      const transfer = await stripe.transfers.retrieve(
        tx.source,
        { stripeAccount: connectedAccountId }
      );
      // 获取原始Charge
      const charge = await stripe.charges.retrieve(transfer.source_transaction);
      // 查询平台业务数据
      const subscriptionRecord = await yourDatabase.query(
        'SELECT customer_name, customer_email, subscription_start, subscription_end FROM subscriptions WHERE stripe_charge_id = $1',
        [charge.id]
      );

      if (subscriptionRecord) {
        payoutDetails.push({
          customerName: subscriptionRecord.customer_name,
          customerEmail: subscriptionRecord.customer_email,
          amount: (transfer.amount / 100).toFixed(2),
          currency: transfer.currency.toUpperCase(),
          period: `${new Date(subscriptionRecord.subscription_start).toLocaleDateString()} - ${new Date(subscriptionRecord.subscription_end).toLocaleDateString()}`
        });
      }
    }

    // 构建邮件内容
    const emailHtml = `
      <div style="max-width: 600px; margin: 0 auto; font-family: Arial, sans-serif;">
        <h2>Hi ${coach.name},</h2>
        <p>你的月度结算已完成,以下是本次结算(Payout ID: ${payout.id})的明细:</p>
        <table style="width: 100%; border-collapse: collapse; margin: 20px 0;">
          <thead>
            <tr style="background-color: #f5f5f5;">
              <th style="padding: 10px; text-align: left; border-bottom: 1px solid #ddd;">客户信息</th>
              <th style="padding: 10px; text-align: left; border-bottom: 1px solid #ddd;">金额</th>
              <th style="padding: 10px; text-align: left; border-bottom: 1px solid #ddd;">服务周期</th>
            </tr>
          </thead>
          <tbody>
            ${payoutDetails.map(item => `
              <tr>
                <td style="padding: 10px; border-bottom: 1px solid #ddd;">${item.customerName} (${item.customerEmail})</td>
                <td style="padding: 10px; border-bottom: 1px solid #ddd;">${item.currency} ${item.amount}</td>
                <td style="padding: 10px; border-bottom: 1px solid #ddd;">${item.period}</td>
              </tr>
            `).join('')}
          </tbody>
        </table>
        <p>本次结算总金额:${(payout.amount / 100).toFixed(2)} ${payout.currency.toUpperCase()}</p>
        <p>如有疑问,请联系平台客服。</p>
      </div>
    `;

    // 发送邮件
    await emailTransporter.sendMail({
      from: '你的平台 <support@yourplatform.com>',
      to: coach.email,
      subject: `月度结算明细(Payout ID: ${payout.id})`,
      html: emailHtml
    });
  }

  res.json({ received: true });
});

关键注意事项

  • 业务数据关联: 必须在你的数据库中记录每笔Stripe Charge/Subscription对应的教练和客户信息,这是关联Payout与客户的核心。
  • 权限配置: 创建Connected账户时,确保申请了transfers:read、charges:read、payouts:read的权限,否则无法调用相关API。
  • 分页处理: 如果Payout包含大量交易,一定要处理listTransactions的分页结果,避免遗漏数据。
  • 金额单位转换: Stripe API返回的金额是最小货币单位(如分),需要除以100转换为常规货币单位。

内容的提问来源于stack exchange,提问作者Ryan Walker

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.04 04:02:05