如何通过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
相关产品推荐
相关产品推荐

