
简介这份资源面向需要在WordPress站点接入PayPal与Stripe支付的站长和开发者重点解决内嵌支付与跳转支付两种模式的落地问题。包内共15个文件以9个php文件为核心配合1个js、1个css及woff/woff2字体资源另有2张png图片整体压缩包约49KB属于轻量级插件或代码片段集合便于直接部署到wp-content/plugins等目录中调试使用。内容围绕支付网关配置、前端交互与样式呈现展开可帮助读者理解内嵌支付无需跳转、跳转支付依托第三方页面完成交易再返回的差异并据此选择适合自身业务的接入方式。目前已有1270人学习下载适合具备一定WordPress与PHP基础、希望快速验证PayPal与Stripe双通道支付流程的开发者参考也可作为二次开发与排错思路的起点。1. 为什么要在 WordPress 里同时接 PayPal 和 Stripe很多做独立站或者内容变现的站长一开始只接一个支付通道结果上线才发现只接 PayPal欧美用户里有一批人没有账号、结账时直接流失只接 Stripe又有一批老用户习惯用 PayPal 余额付款转化率上不去。更麻烦的是WordPress 生态里插件五花八门有的只支持跳转、有的只支持内嵌混用之后回调地址打架、订单状态错乱排查起来像开盲盒。这篇要讲的就是把 PayPal 和 Stripe 同时接进 WordPress并且两种通道都支持「内嵌支付」和「跳转支付」两种形态。内嵌指的是用户在你自己页面上直接填卡或点按钮完成付款不离开站点跳转指的是跳到支付服务商的托管页面完成后再跳回来。前者体验顺滑、转化高后者实现简单、风控压力小。适合谁看用 WooCommerce 或自建表单做收款、需要覆盖多地区用户、又不想被单一通道绑死的开发者。下面按「先跑通最小闭环再抠参数和坑」的顺序讲。2. 通道选型与两种支付形态的取舍2.1 内嵌和跳转到底差在哪内嵌支付Embedded / Elements的本质是支付服务商把一段 JS 组件挂到你的页面上卡号、有效期这些敏感字段其实是在服务商的 iframe 里输入的你的服务器碰不到原始卡号所以 PCI 合规压力小。用户全程不离开你的域名页面风格统一移动端体验也好。代价是你得处理前端加载失败、组件初始化超时、以及异步回调的时序问题。跳转支付Redirect / Hosted Checkout则是你把订单信息发给服务商服务商返回一个托管页地址用户跳过去付款付完再跳回你指定的 return_url。实现上就是一次表单提交或一次 API 调用拿 URL逻辑简单、兼容性极好老浏览器、老主题都不容易翻车。缺点是跳出站点那一下会掉一部分转化而且回跳的瞬间订单状态还没最终确定得靠 webhook 兜底。我的建议是主流程用内嵌把跳转作为降级方案。前端检测到组件加载失败或超时自动切到跳转链接这样既保体验又保成功率。2.2 为什么两个通道都要接单一通道的风险不只是转化率。PayPal 的风控策略和 Stripe 完全不同某些地区、某些卡种在一边被拒在另一边可能就过了。同时接两个等于给用户多一个「换一种方式再试」的出口。实现上不要让用户自己选通道选半天常见做法是页面上并排两个按钮或者先默认一个、失败后提示另一个。从对账角度两个通道的订单号体系要统一。我一般会在自己的订单表里存一个内部 order_id再分别存 paypal_txn_id 和 stripe_payment_intent_idwebhook 回来时按内部 order_id 归集避免两套 ID 混在一起对不上账。2.3 最小可跑通的目录与依赖不管用插件还是自己写先把依赖理清楚。下面是一个自建支付处理的最小结构放在主题目录下的inc/payments/里inc/payments/ ├── paypal-embed.php # PayPal 内嵌按钮与跳转 ├── stripe-embed.php # Stripe Elements 与 Checkout 跳转 ├── webhook-router.php # 统一接收两个通道的 webhook └── order-store.php # 内部订单表读写依赖上PayPal 用官方 JS SDKhttps://www.paypal.com/sdk/jsStripe 用https://js.stripe.com/v3/。服务端如果要调 APIPayPal 走 REST需要 client_id / secret 换 access_tokenStripe 用 secret key 直接调。密钥一律放wp-config.php常量或环境变量不要写进数据库明文。提示两个 SDK 都必须在 HTTPS 下加载本地调试可以用自签证书但上线前务必确认证书链完整否则内嵌组件会静默失败。3. 用代码把内嵌支付跑起来3.1 PayPal 内嵌按钮的最小实现先在页面里挂 SDK再渲染按钮。下面这段是前端部分放在结算页模板里// 加载 PayPal SDKcurrency 按你的定价币种改 const script document.createElement(script); script.src https://www.paypal.com/sdk/js?client-idYOUR_CLIENT_IDcurrencyUSDcomponentsbuttons; script.onload () { paypal.Buttons({ // 创建订单把内部 order_id 传给后端由后端调 PayPal 创建 createOrder: async () { const res await fetch(/wp-json/mypay/v1/paypal/create, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ order_id: window.internalOrderId }) }); const data await res.json(); return data.paypal_order_id; // 返回给 SDK }, // 用户批准后捕获资金 onApprove: async (data) { await fetch(/wp-json/mypay/v1/paypal/capture, { method: POST, headers: { Content-Type: application/json }, body: JSON.stringify({ paypal_order_id: data.orderID }) }); window.location.href /thank-you?order window.internalOrderId; }, onError: (err) { // 内嵌失败降级到跳转 console.error(PayPal embed failed, err); window.location.href /paypal-redirect?order window.internalOrderId; } }).render(#paypal-button-container); }; document.head.appendChild(script);逻辑说明createOrder不在前端拼金额而是把内部 order_id 发给后端由后端查库拿到金额再调 PayPal 创建订单这样前端改不了价格。onApprove里做 capture成功后跳感谢页。onError是降级入口内嵌挂了就跳托管页。参数说明currency必须和你在 PayPal 后台设置的收款币种一致不一致会报CURRENCY_NOT_SUPPORTED。componentsbuttons只加载按钮组件减小体积如果你还要用卡字段组件再加componentsbuttons,card-fields。client-id是公开的可以出现在前端但secret绝对不能。3.2 Stripe Elements 内嵌卡支付Stripe 的内嵌用 Payment Intent Elements。后端先创建 Payment Intent把 client_secret 给前端// 后端创建 Payment Intent function mypay_create_stripe_intent($order_id) { $order mypay_get_order($order_id); $amount intval($order-total * 100); // Stripe 用最小货币单位美元是分 $resp wp_remote_post(https://api.stripe.com/v1/payment_intents, [ headers [ Authorization Bearer . STRIPE_SECRET_KEY, Content-Type application/x-www-form-urlencoded, ], body [ amount $amount, currency usd, metadata [internal_order_id $order_id], ], ]); $body json_decode(wp_remote_retrieve_body($resp), true); return $body[client_secret] ?? ; }前端拿到 client_secret 后挂载 Elementsconst stripe Stripe(YOUR_PUBLISHABLE_KEY); const elements stripe.elements(); const card elements.create(card); card.mount(#card-element); document.getElementById(pay-btn).addEventListener(click, async () { const { paymentIntent, error } await stripe.confirmCardPayment(clientSecret, { payment_method: { card: card } }); if (error) { // 卡被拒或验证失败提示用户换卡或换通道 document.getElementById(err).textContent error.message; } else if (paymentIntent.status succeeded) { window.location.href /thank-you?order window.internalOrderId; } });逻辑说明金额在后端算好传给 Stripe前端只负责确认支付。metadata里塞内部 order_idwebhook 回来时能直接对应。参数上amount是整数分别传小数否则报amount must be an integer。currency小写。3.3 跳转支付的兜底实现跳转版 PayPal 就是拿一个 approve 链接$resp wp_remote_post(https://api.paypal.com/v2/checkout/orders, [ headers [ Authorization Bearer . mypay_get_paypal_token(), Content-Type application/json, ], body wp_json_encode([ intent CAPTURE, purchase_units [[ amount [currency_code USD, value number_format($order-total, 2, ., )], custom_id $order_id, ]], application_context [ return_url home_url(/paypal-return), cancel_url home_url(/paypal-cancel), ], ]), ]); $body json_decode(wp_remote_retrieve_body($resp), true); $approve ; foreach ($body[links] as $link) { if ($link[rel] approve) { $approve $link[href]; } } wp_redirect($approve);Stripe 跳转更简单用 Checkout Session把success_url和cancel_url设好返回的url直接跳。注意custom_id和metadata是回跳后找回订单的关键别漏。4. 回调、webhook 与订单状态机4.1 为什么不能只信前端回跳用户付完款跳回 thank-you 页这个动作本身不可信——用户可能直接手输 URL也可能网络断了没跳回来。真正确定订单已付的是服务端的 webhook。所以状态机要这样设计前端回跳只负责展示「处理中」订单最终置为「已支付」必须等 webhook 校验通过。PayPal 的 webhook 事件看CHECKOUT.ORDER.APPROVED和PAYMENT.CAPTURE.COMPLETEDStripe 看payment_intent.succeeded。收到后先验签再按内部 order_id 更新状态且要做幂等——同一个事件可能重发多次。4.2 统一 webhook 路由的写法// webhook-router.php add_action(rest_api_init, function () { register_rest_route(mypay/v1, /webhook/(?Pchannelpaypal|stripe), [ methods POST, callback mypay_handle_webhook, permission_callback __return_true, ]); }); function mypay_handle_webhook($request) { $channel $request[channel]; $raw $request-get_body(); if ($channel stripe) { // Stripe 验签用 endpoint secret 和 Stripe-Signature 头 $sig $_SERVER[HTTP_STRIPE_SIGNATURE] ?? ; if (!mypay_verify_stripe_sig($raw, $sig)) { return new WP_REST_Response([ok false], 400); } $event json_decode($raw, true); if ($event[type] payment_intent.succeeded) { $oid $event[data][object][metadata][internal_order_id]; mypay_mark_paid($oid, $event[data][object][id], stripe); } } else { // PayPal 验签走 verify-webhook-signature 接口 if (!mypay_verify_paypal_sig($raw, $_SERVER)) { return new WP_REST_Response([ok false], 400); } $event json_decode($raw, true); if ($event[event_type] PAYMENT.CAPTURE.COMPLETED) { $oid $event[resource][custom_id]; mypay_mark_paid($oid, $event[resource][id], paypal); } } return new WP_REST_Response([ok true], 200); }逻辑说明一个路由按 channel 分流各自验签后再处理。mypay_mark_paid内部先查订单当前状态已是 paid 就直接返回保证幂等。参数上Stripe 验签需要endpoint secret在 Dashboard 的 webhook 设置里生成PayPal 验签要调它的 verify 接口并传transmission_id、transmission_time、cert_url、auth_algo、transmission_sig这几个头。4.3 订单状态与对账字段内部订单表至少要有这些字段方便两个通道归集字段类型说明order_idvarchar内部唯一单号channelvarcharpaypal / stripetxn_idvarchar通道侧交易号amountdecimal金额统一存基准币种statusvarcharpending / paid / refundedcreated_atdatetime创建时间对账时按created_at拉一天的单分别和两个通道的流水比对差异单人工核。退款也要走 webhook别只在前端点退款就完事。5. 避坑与常见问题排查5.1 内嵌组件加载不出来现象页面空白控制台报Failed to load resource或组件容器一直转圈。原因通常是 SDK 域名被主题的 CSP 策略拦了或者页面不是 HTTPS。解决检查Content-Security-Policy里有没有放行paypal.com和js.stripe.com本地调试确认证书有效。另一个常见原因是容器 div 在 SDK 加载完成时还不存在把 render 放到 DOMContentLoaded 之后。5.2 金额对不上、报币种错误现象PayPal 报CURRENCY_NOT_SUPPORTEDStripe 报amount must be an integer。原因是前端传了小数金额或者币种和后台设置不一致。解决金额一律在后端算Stripe 转成整数分PayPal 用两位小数字符串币种在后台和代码里保持同一个值别一处 USD 一处 USDC。5.3 webhook 收不到或验签失败现象订单一直 pending日志里没有 webhook 记录。原因可能是站点有防火墙拦了外部 POST或者验签用的 secret 和 Dashboard 里不是同一个。解决先在通道后台的 webhook 日志里看投递状态如果是 4xx 就查验签如果是超时检查服务器有没有屏蔽通道的出口 IP。验签失败最常见的是拿原始 body 时被框架改写过务必用get_body()拿原始字符串再验。5.4 重复回调导致重复发货现象用户收到两封发货邮件或者积分加了两遍。原因是 webhook 重发而处理逻辑没有幂等。解决mypay_mark_paid里先SELECT ... FOR UPDATE锁行判断 status 已是 paid 就直接返回或者用 txn_id 建唯一索引插入冲突就忽略。5.5 跳转回来后订单还是 pending现象用户明明付了回跳页面却显示未支付。原因是回跳只是浏览器行为webhook 可能延迟几秒到几十秒。解决thank-you 页做轮询每 3 秒查一次订单状态最多查 10 次同时页面文案写「支付确认中请勿重复付款」避免用户以为失败又付一次。6. 进阶把两个通道收敛成一套接口6.1 抽象一层支付网关写到后面你会发现PayPal 和 Stripe 的差异其实可以收敛。我一般定义一个PaymentGateway接口方法就四个create($order)、capture($txn)、refund($txn, $amount)、verify_webhook($raw, $headers)。PayPal 和 Stripe 各实现一份上层结算逻辑只调接口不关心具体通道。这样以后要加第三个通道只写一个新实现类不用动主流程。6.2 用配置表控制内嵌与跳转的开关把「内嵌优先、失败降级跳转」做成配置而不是写死在代码里配置项取值作用embed_enabledtrue/false是否启用内嵌fallback_redirecttrue/false内嵌失败是否降级embed_timeout_ms整数组件加载超时阈值preferred_channelpaypal/stripe默认展示哪个超时阈值我一般设 5000ms太短会误判太长用户等得烦。降级逻辑放在前端onError和后端创建订单失败两个位置双保险。6.3 一个验证支付链路是否健康的小技巧上线后别只看「有没有报错」要主动验证。我习惯写一个定时任务每天用沙箱环境跑一笔最小金额的测试单走完内嵌和跳转两条路径检查订单状态是否最终变为 paid、webhook 是否收到、对账字段是否齐全。任何一环失败就发告警。这个习惯帮我提前发现过好几次证书过期和 secret 轮换导致的静默失败——支付这东西等用户投诉才发现就晚了。沙箱和生产的密钥要严格隔离测试单用独立的内部 order_id 前缀比如TEST-对账时直接排除别混进真实流水。这套跑顺之后加通道、改金额、调文案都不会再动到支付核心心里踏实很多。希望帮到你。本文还有配套的精品资源点击获取