--- title: 内嵌 SDK permalink: /notes/zh/integrate/sdk-v4-preview/ createTime: '2026/07/09 10:00:00' description: '以内嵌 SDK 为统一入口,通过 Web/WAP、iOS、Android 平台切换展示不同端的接入路径和完整指南入口。' outline: [2, 4] --- PingPong 内嵌 SDK 是基于 SDK 集成的支付组件,旨在帮助商户在自有 Web 页面或移动 App 内快速承载收银台能力,减少支付方式展示、支付交互和客户端适配成本,并为买家提供更连贯的支付体验。 - Web/WAP 端:适用于浏览器或 H5 页面环境,通过 JavaScript-SDK 在商户页面内渲染 PingPong 收银台。 - App 端(iOS / Android):适用于商户自有移动应用,通过 Native SDK 在 App 内唤起半屏收银台。 各端内嵌 SDK 支持的主要能力如下表所示: | 特性 | 描述 | Web/WAP | iOS | Android | |------|------|:-------:|:---:|:-------:| | 支付方式列表渲染 | SDK 根据支付会话展示可用支付方式,商户无需自行维护支付方式列表和基础交互。 | 支持 | 支持 | 支持 | | 站内嵌入体验 | 支付表单直接嵌入商户 Web/H5 页面,买家可在商户页面内完成支付操作。 | 支持 | 不支持 | 不支持 | | 半屏收银台体验 | 在商户 App 内唤起 PingPong Native SDK 半屏支付面板,支付流程在 App 内完成。 | 不支持 | 支持 | 支持 | | 界面与主题配置 | 支持按端侧能力配置收银台展示效果;Web/WAP 可通过 `customizeConfig`、`customizeStyles` 等配置布局、主题和交互。 | 支持 | 不支持 | 不支持 | | 自定义支付按钮 | Web/WAP 可隐藏 SDK 原生支付按钮,并通过 `PingPong.Checkout.pay.run()` 由商户自定义按钮触发支付。 | 支持 | 不支持 | 不支持 | | 支付结果回调 | Web/WAP 可通过 `checkoutResultHook` 接收前端支付结果;iOS / Android 可通过客户端回调接收结果。商户服务端仍应以支付通知或交易查询确认最终交易状态。 | 支持 | 支持 | 支持 | ## 平台选择 ## Web/WAP ## 接入摘要 内嵌 SDK(预下单)是一种低代码站内收银台方案。商户服务端先调用 [prePay(预下单)接口](/notes/zh/checkout/api/reserve/) 创建支付会话,再由前端通过 JavaScript-SDK 在商户页面内直接渲染 PingPong 收银台。该方案既能保留买家在商户站内完成支付的体验,也能减少商户自建支付页、支付方式展示和支付交互处理的复杂度。 ::: tip 适用场景 适合希望将收银台直接嵌入商户页面、降低支付页前端开发成本并保留站内支付体验的团队;若可接受买家跳转至 PingPong 托管收银台完成支付,建议选择[跳转收银台](/notes/zh/integrate/link/)。 ::: ## 支付体验 ### Web 支付 在内嵌SDK模式下,您可以将结账体验直接嵌入到自己的网站中。在网页端,结账页面会显示在您的网站内,使客户无需跳转至独立的托管页面即可完成支付。 ![Web/WAP PC 支付体验示意图](/images/integrate/sdk-experience/jssdk_web_ui.png) ### Mobile 支付 在内嵌SDK模式下,允许您将结账体验无缝集成至自有网站中。针对移动端小屏幕进行了专属 UI 优化,客户在手机网页或 App 内即可完成支付,全程无需跳转至外部托管页面,从而提供更流畅、沉浸的移动端支付体验。 ![Web/WAP Mobile 支付体验示意图](/images/integrate/sdk-experience/jssdk_mobile_ui.png) ## 支付流程 ```mermaid %% agent-tag: embedded-sdk-checkout-flow sequenceDiagram participant Cardholder as 🙋 持卡人 participant UserTerminal as 📱 用户终端 participant Merchant as 🏪 商户/建站平台 participant PP as 🔄 PingPongCheckout participant Issuer as 🏦 ISSUER Cardholder->>UserTerminal: 1. 发起结账 UserTerminal->>Merchant: 2. 提交订单 Merchant->>PP: 3. 请求下单接口(POST /v4/payment/prePay) PP-->>Merchant: 4. 返回收银台参数(token / innerJsUrl / paymentUrl) Merchant-->>UserTerminal: 5. 返回收银台初始化参数 UserTerminal->>PP: 6. 通过 JS-SDK 渲染收银台 PP-->>UserTerminal: 7. 展示收银台页面 Cardholder->>UserTerminal: 8. 选择支付方式并填写支付信息 UserTerminal->>PP: 9. 确认支付 alt 3D流程 PP-->>UserTerminal: 10. 跳转到 3D 挑战页 Cardholder->>UserTerminal: 11. 完成 3D 验证 UserTerminal->>PP: 12. 提交 3D 验证结果 PP->>Issuer: 13. issuer 验证数据 Issuer-->>PP: 14. 返回验证结果 PP-->>UserTerminal: 15. 触发 checkoutResultHook 或渲染支付结果页 UserTerminal->>Merchant: 16. 展示商户结果页 / 跳转 payResultUrl Merchant-->>UserTerminal: 17. 根据查询/通知结果处理业务逻辑 else 非3D流程 PP-->>UserTerminal: 18. 触发 checkoutResultHook 或渲染支付结果页 UserTerminal->>Merchant: 19. 展示商户结果页 / 跳转 payResultUrl Merchant-->>UserTerminal: 20. 根据查询/通知结果处理业务逻辑 end PP->>Merchant: 21. 异步通知支付结果(notificationUrl) Merchant-->>PP: 22. 返回 HTTP 200 ``` ::: steps 1. 买家下单 - 买家在商户页面发起结账并提交订单。 - 商户侧开始创建支付会话,准备初始化内嵌收银台。 2. 创建支付会话 - 商户服务端调用 [prePay(预下单)接口](/notes/zh/checkout/api/reserve/)。 - 获取 `token`、`innerJsUrl` 等收银台初始化所需参数。 3. 初始化并展示收银台 - 商户前端加载 JavaScript-SDK,并将 `accessToken` 与 `locale` 传给 `pp-checkout`。 - PingPong 收银台在商户页面内完成渲染并展示可用支付方式。 4. 买家完成支付 - 买家选择支付方式、填写支付信息并确认支付。 - 若触发 3D Secure,买家在当前支付流程中完成验证。 5. 确认支付结果 - 支付完成后,前端可通过 `checkoutResultHook` 接收 SDK 返回的前端支付结果并展示商户自有结果页;未配置 Hook 时,仍按原有结果页或 `payResultUrl` 逻辑处理。 - 商户服务端仍需结合异步通知或查询接口确认最终支付状态。 ::: ::: warning 关键注意事项 1. 支付结果以异步通知(`notificationUrl`)或对应查询接口确认结果为准,`checkoutResultHook` 和前端结果页仅用于买家展示; 2. 优先使用 `prePay(预下单)接口` 返回的 `bizContent.innerJsUrl` 动态加载 SDK,以避免环境切换或版本变更带来的地址不一致问题; 3. `ready`、`error` 等事件监听器需要在设置 `accessToken` 之前注册,否则可能错过初始化事件; 4. 在 `locale` 未传入的情况下,收银台默认展示英文;如果传了 `locale`,则以传入值为准。具体枚举值可参考[语言列表](/notes/zh/appendix/languageCode/)。 ::: ## 集成步骤 ### 步骤 1:创建支付会话 在初始化 JS-SDK 收银台之前,商户服务端需要先调用 **prePay(预下单)接口** 获取 SDK 初始化所需的关键参数。 支付请求关键参数: | 参数 | 必填 | 说明 | |:---|:---:|:---| | `captureDelayHours` | O | 资金请款方式。默认自动请款;如需手动请款,请按支付方式能力和接口规则配置 | | `amount` | M | 交易金额 | | `currency` | M | ISO 4217 三位交易币种 | | `merchantTransactionId` | M | 商户网站订单流水号,订单唯一标识 | | `shopperIP` | M | 买家下单 IP | | `merchantUserId` | M | 商户侧用户唯一 ID | | `goods` | M | 商品信息,至少包含商品名称、单价和数量 | | `customer` | C | 买家信息。不同支付方式可能要求邮箱、手机号或账单信息,请以目标支付方式要求为准 | | `paymentMethods` | O | 指定收银台可展示的支付方式范围,不传或为空时按店铺配置展示 | | `notificationUrl` | O | 支付结果通知地址,建议传入 | | `payResultUrl` | O | 买家完成支付流程后的商户结果页地址 | | `language` | O | 收银台展示语言,未传入时默认展示英文,枚举值参考[语言列表](/notes/zh/appendix/languageCode/) | 有关完整参数的更多信息,请参阅 [prePay(预下单)接口](/notes/zh/checkout/api/reserve/)。 支付请求示例: ```json { "captureDelayHours": "0", "amount": "100", "currency": "USD", "merchantTransactionId": "{{merchantTransactionId}}", "payResultUrl": "https://test-acquirerpay.pingpongx.com/qa/result.html", "notificationUrl": "https://test-acquirerpay.pingpongx.com/qa/result.html", "language": "en", "shopperIP": "222.126.52.23", "merchantUserId": "USER_12345", "customer": { "email": "buyer@gmail.com" }, "paymentMethods": [], "goods": [ { "name": "商品名称mepsking1", "description": "商品描述", "unitPrice": "1", "number": "1", "virtualProduct": "Y", "imgUrl": "https://xiu.mepsking.top/material/1/16606169805015585.png" } ] } ``` 支付响应示例: ```json { "amount": "1000", "currency": "USD", "transactionId": "2023092050004591", "merchantTransactionId": "PMT-PR5GFUNZQP1695182276082", "token": "EU:vr_YVR8u7rn7C1gG97DOg9_-Y66ubtNtoayJ_wiEEzdCnxCHYIk0pXordJYBjq1g", "paymentUrl": "https://sandbox-acquirer-payment-ssr.pingpongx.com/v3/checkout?token=...", "innerJsUrl": "https://paycdn.pingpongx.com/production/static/sdk/ppPay.min.js?token=..." } ``` 支付响应关键字段: | 参数 | 说明 | |:---|:---| | `transactionId` | PingPong 交易流水号,用于后续查询、退款、对账 | | `merchantTransactionId` | 商户网站订单流水号 | | `token` | SDK 初始化令牌,用于设置 `pp-checkout` 的 `accessToken` 属性 | | `innerJsUrl` | 动态加载 SDK 的 URL,推荐使用此地址加载 JavaScript-SDK | | `paymentUrl` | 支付页面 URL;如业务后续切换为跳转收银台模式,可直接复用该地址 | ### 步骤 2:引入Javascript-SDK 复制以下代码,通过CDN地址引入 PingPongCheckout Javascript-SDK ::: code-tabs @tab 🧪 沙箱环境 ```js ``` @tab 🇪🇺 FRA 生产环境 ```js ``` @tab 🇸🇬 SG 生产环境 ```js ``` @tab 🇺🇸 US 生产环境 ```js ``` ::: #### 动态获取 SDK 地址(推荐) 除了使用固定的 CDN 地址外,**推荐**从 `prePay(预下单)接口` 的响应中动态获取 SDK 地址。 调用 **prePay(预下单)接口**(`POST /v4/payment/prePay`)成功后,响应中会返回 `bizContent.innerJsUrl` 字段,该字段包含当前环境对应的 JS-SDK 地址。 ```js:line-numbers title="src/utils/loadSDK.js" async function initCheckoutWithDynamicSDK() { // 1. 调用服务端 prePay(预下单)接口 const response = await fetch('/api/v4/payment/prePay', { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ accId: '2018092714313010016291', clientId: '2018092714313010016', signType: 'SHA256', version: '1.0', bizContent: { amount: '100', currency: 'USD', merchantTransactionId: 'ORDER_' + Date.now(), notificationUrl: 'https://your-domain.com/notify', payResultUrl: 'https://your-domain.com/result.html', customer: { email: 'customer@example.com', firstName: 'John', lastName: 'Doe' } } }) }); const data = await response.json(); // 检查响应状态 if (data.code !== '000000') { console.error('预下单失败:', data.description); // [!code error] return; } const { token, innerJsUrl } = data.bizContent; // 2. 动态加载 JS-SDK await loadScript(innerJsUrl); // [!code focus] // 3. 配置收银台 const ppCheckout = document.querySelector('pp-checkout'); ppCheckout.setAttribute('accessToken', token); ppCheckout.setAttribute('locale', 'en'); } /** * 动态加载脚本 * @param {string} src - 脚本地址 * @returns {Promise} */ function loadScript(src) { return new Promise((resolve, reject) => { const script = document.createElement('script'); script.type = 'module'; script.src = src; script.onload = resolve; script.onerror = reject; document.head.appendChild(script); }); } initCheckoutWithDynamicSDK(); ``` ::: tip 提示 使用动态获取的 SDK 地址可以确保始终使用与当前环境匹配的最新版本,无需在代码中硬编码环境地址。 ::: ### 步骤 3:初始化并渲染收银台 ::: note 提示 从沙箱环境切换到生产环境时,请务必检查并且完成下列的操作,否则会导致收银台无法正常渲染。 - 将引入Javascript-SDK的CDN地址切换到生产环境指定的URL ::: 1. 当你在联调**沙箱环境**时,需要在引入**沙箱环境** 的PingPongCheckout Javascript-SDK地址(发布到生产环境的时候别忘了切换成**生产环境**的地址) 2. 将 `pp-checkout` 标签插入 html body 中 ```html:line-numbers title="index.html" ``` 3. 将预下单获取到的 `accessToken` 传入,接口文档详见prePay(预下单)接口 ```html:line-numbers title="index.html" ``` 4. 你可以将收银台所要展示的语种(默认为英文,更多语言详见Locale)通过标签属性的方式传递给 `pp-checkout`,如下: ```html:line-numbers title="index.html" ``` 通过以上步骤,你已经成功渲染了 Javascript-SDK 收银台。 ### 步骤 4:收银台行为与界面配置 在使用全局变量前,请确保Javascript-SDK 加载完成。 #### customizeConfig 布局与界面配置 通过 `PingPong.Checkout.customizeConfig` 可以自定义收银台的布局和界面元素: | 配置项 | 类型 | 默认值 | 说明 | |-------|------|-------|------| | `layout` | `'tab'\|'accordion'` | `'tab'` | 页面布局样式,可选值:`"tab"`(标签页样式)、`"accordion"`(平铺样式) | | `displayCheckoutHeader` | `boolean` | `true` | 是否显示收银台顶部标题栏 | | `originalPay` | `boolean` | `true` | 是否显示原生支付按钮 | | `toPingPongResult` | `boolean` | `true` | 支付完成后是否跳转到 PingPong 结果页。`false` 表示直接跳转到商户配置的结果页面 | | `hideStoredCards` | `boolean` | `false` | 是否隐藏 COF(Card On File)列表 | | `onlyDisplaySavedCard` | `boolean` | `false` | 设为 `true` 时,收银台仅展示已存储的卡信息区域(隐藏新卡输入)。适用于 [复购 CVV 收集收银台](/notes/zh/integrate/tokenization/cardOnFileCVV/) | | `disableCardRemoval` | `boolean` | `false` | 设为 `true` 时,禁止用户删除已存储的卡信息。适用于 [复购 CVV 收集收银台](/notes/zh/integrate/tokenization/cardOnFileCVV/) | | `displayCardPrompt` | `boolean` | `true` | 是否显示卡支付提示信息 | | `localizationErrorMsg` | `boolean` | `false` | 是否翻译支付错误消息 | | `displayCardsLogo` | `boolean` | `true` | 是否显示卡品牌 logo 列表 | | `isChallengeIframe` | `boolean` | `false` | 是否使用 iframe 展示 3DS Challenge 页面。设为 `true` 时,将通过 iframe 承载 Challenge 流程 | ::: warning 注意 `toPingPongResult` 配置需要在**服务端创建支付会话时**设置。客户端设置可能会被服务端配置覆盖。如需禁用 PingPong 结果页跳转,请确保服务端已正确配置。 ::: ```js:line-numbers title="src/config/payment.js" // [!code highlight:1] customizeConfig 布局与界面配置 PingPong.Checkout.customizeConfig = { // [!code focus] layout: "accordion", // 平铺样式 displayCheckoutHeader: false, // 隐藏标题栏 originalPay: false, // 隐藏原生支付按钮,需自定义按钮触发支付 toPingPongResult: false, // 支付完成后不跳转到 PingPong 结果页 hideStoredCards: false, // 显示已保存的卡列表 displayCardPrompt: true, // 显示卡支付提示信息 localizationErrorMsg: false, // 不翻译错误消息 displayCardsLogo: true, // 显示卡品牌 logo isChallengeIframe: true // 使用 iframe 展示 3DS Challenge 页面 }; ``` ##### 自定义支付按钮(可选) 当 `originalPay` 设置为 `false` 时,需要自定义按钮点击事件来触发支付。 ```js:line-numbers title="src/config/payment.js" // 初始化参数中 originalPay 为 false 时, 需自定义支付按钮点击事件 document.querySelector('#pay').onclick = function () { // [!code focus] PingPong.Checkout.pay.run() // [!code focus] } ``` #### customizeStyles 主题样式 通过 `PingPong.Checkout.customizeStyles` 可以自定义收银台的视觉样式: | 配置项 | 类型 | 说明 | 示例值 | |-------|------|------|-------| | `themeColor` | `string` | 主题色,支持 CSS 颜色值 | `"rgb(26, 29, 37)"`, `"#1a1d25"` | | `themeColorLight` | `string` | 主题色浅色变体 | `"rgba(26, 29, 37, 0.4)"` | | `fontFamily` | `string` | 字体系列 | `"'Montserrat', sans-serif"` | ```js:line-numbers title="src/config/styles.js" // [!code highlight] 品牌定制:深色主题 PingPong.Checkout.customizeStyles = { themeColor: "rgb(26, 29, 37)", // 主题色 themeColorLight: "rgba(26, 29, 37, 0.4)", // 主题色浅色变体 fontFamily: "'Montserrat', 'PingFang SC', 'Microsoft YaHei', sans-serif" }; ``` ::: tip 提示 主题色和字体配置需在 SDK 加载完成后、设置 `accessToken` 之前进行。 ::: #### PingPong.Checkout.beforeCheckoutHook type: ```js (() => void) | (() => Promise) ``` `beforeCheckoutHook` 用来设置发起支付请求前的钩子函数。 当你在用户点击支付按钮,发起支付请求前,需要执行你自己的业务逻辑,如:上报埋点、检查库存等,可以设置该钩子函数。 该函数可以返回一个`Promise`,后续的支付流程会等待该 Promise 状态变为 Fulfilled 后才会继续执行。如果你想在 Promise 状态为 Rejected 或者异步结果不满足你的业务条件时,可以抛出异常,SDK在捕获到异常后中断支付流程。 ```js:line-numbers title="src/hooks/beforeCheckout.js" // [!code highlight] 支付前钩子:检查库存 PingPong.Checkout.beforeCheckoutHook = () => { // [!code focus] return fetch('/api/requestInventory').then(res => { // [!code focus] const { inventoryQuantity } = res; if(inventoryQuantity < MIN_QUANTITY) { throw new Error('库存不足,需中断交易') // [!code error] } }).catch((error) => { throw new Error('接口异常,需中断交易') // [!code error] }) }; ``` #### PingPong.Checkout.checkoutResultHook type: ```ts (payload: CheckoutResultHookPayload) => void | Promise ``` `checkoutResultHook` 用于接收 SDK 前端可直接得到的支付结果,并由商户前端接管支付完成后的页面展示或跳转。请在用户发起支付前注册该 Hook。该 Hook 挂载在 `PingPong.Checkout` 上,不是 `` 标签属性。 `checkoutResultHook` 会把支付结果统一收敛为以下三种状态: | 状态 | 含义 | 建议处理 | |:---|:---|:---| | `success` | 支付成功 | 展示商户成功页,或跳转到商户成功页 | | `pending` | 支付处理中或待确认,包含 SDK 内部的 `PENDING`、`PROCESSING`、`REVIEW` 等非终态场景 | 展示处理中页面,并通过服务端查询或异步通知确认最终结果 | | `failed` | 支付失败 | 展示失败页,提供重试或返回商户站点的入口 | Payload 类型定义: ```ts:line-numbers title="src/types/checkout-result.ts" type CheckoutResultHookPayload = { status: 'success' | 'pending' | 'failed'; code: string; message: string; merchantTransactionId?: string; paymentMethod?: string; }; ``` Payload 字段说明: | 字段 | 类型 | 是否必返 | 说明 | |:---|:---|:---:|:---| | `status` | `success` \| `pending` \| `failed` | M | SDK 归一化后的前端支付结果 | | `code` | `string` | M | 支付结果码或错误码;无特定错误时可能返回 `0` | | `message` | `string` | M | 支付结果描述或错误描述 | | `merchantTransactionId` | `string` | O | 商户订单号 | | `paymentMethod` | `string` | O | 支付方式 | Payload 示例: ```json { "status": "pending", "code": "0", "message": "Payment is processing", "merchantTransactionId": "ORDER_202607200001", "paymentMethod": "VISA" } ``` 接入示例: ```js:line-numbers title="src/hooks/checkoutResult.js" PingPong.Checkout.checkoutResultHook = async (payload) => { // [!code focus] const orderNo = encodeURIComponent(payload.merchantTransactionId || ''); switch (payload.status) { case 'success': window.location.href = `/payment/success?orderNo=${orderNo}`; break; case 'pending': window.location.href = `/payment/pending?orderNo=${orderNo}`; break; case 'failed': window.location.href = `/payment/failed?code=${encodeURIComponent(payload.code)}`; break; default: window.location.href = `/payment/result?orderNo=${orderNo}`; break; } }; ``` 触发与跳转规则: | 商户配置 | `success` / `pending` 场景 | `failed` 场景 | 结果页跳转 | |:---|:---|:---|:---| | 未配置 Hook | 不触发 Hook | 不触发 Hook | 保持原有结果页跳转逻辑 | | 仅配置 `checkoutResultHook` | 触发 `checkoutResultHook` | 触发 `checkoutResultHook` | 命中 Hook 后不再跳转 PingPong 结果页 | | 仅配置 `checkoutFailedHook` | 不触发 Hook | 触发 `checkoutFailedHook` | 保持原有失败处理逻辑 | | 同时配置两个 Hook | 触发 `checkoutResultHook` | 优先触发 `checkoutFailedHook` | 失败场景不重复回调 | 建议新接入商户优先配置 `checkoutResultHook`。如果历史上已经接入 `checkoutFailedHook`,请在确认业务兼容后再迁移到 `checkoutResultHook`,避免同一失败结果被重复处理。 ::: warning 注意 - `checkoutResultHook` 是浏览器侧回调,不能作为商户后台更新订单状态的唯一依据。最终支付结果请以 PingPong 服务端异步通知或订单查询接口为准。 - 如果同时配置 `checkoutResultHook` 和 `checkoutFailedHook`,支付失败场景会优先触发 `checkoutFailedHook`,SDK 不会再重复触发 `checkoutResultHook`。如需由一个 Hook 统一处理 `success`、`pending`、`failed`,请只配置 `checkoutResultHook`。 - `pending` 不是失败。请展示处理中状态,并在商户后台继续等待最终支付结果。 - Hook 支持返回 `Promise`,SDK 会等待 Promise 完成。请避免在 Hook 内执行长时间阻塞逻辑。 - Hook 内部异常不会恢复 PingPong 结果页跳转。商户应在 Hook 内自行处理异常,并准备兜底页面。 - 需要跳转到 3DS 认证页面,或渠道返回 `redirectUrl`、中间页地址时,SDK 仍会继续执行原有跳转流程。 ::: #### PingPong.Checkout.checkoutFailedHook type: ```js (() => void) | (() => Promise) ``` checkoutFailedHook 接收以下参数: ```js (code: string, message: string) => void | Promise; // code: string - 错误码 // message: string - 错误消息 ``` `checkoutFailedHook` 用来自定义错误逻辑 当用户支付失败时,PingPong 默认会弹窗提示用户失败原因。如果你想自定义弹窗 UI 或文本,可以设置该钩子函数。 该函数可以返回一个 Promise。如果返回 Promise,后续的流程会等待该 Promise 状态变为 Fulfilled 后才继续执行 ```js:line-numbers title="src/hooks/checkoutFailed.js" // [!code highlight] 支付失败钩子:自定义错误提示 PingPong.Checkout.checkoutFailedHook = (code: string, message: string) => { // [!code focus] notification.open({ // [!code focus] message: 'Error title', description: `${code}: ${message}` // [!code warning] }) }; ``` #### 事件监听(可选) SDK 支持监听初始化过程中的 `ready` 和 `error` 事件,方便外部进行状态管理和错误处理。 ```js:line-numbers title="src/config/payment.js" // 监听 SDK 初始化事件 // 监听初始化成功事件 document.querySelector('pp-checkout').addEventListener('ready', (e) => { console.log('SDK初始化成功'); // 初始化成功回调 // 可以在此处执行初始化完成后的逻辑,如隐藏加载动画 }); // 监听初始化失败事件 document.querySelector('pp-checkout').addEventListener('error', (e) => { console.log('SDK初始化失败', e.detail); // 错误信息 // 可以在此处执行错误处理逻辑,如显示错误提示、重试等 }); ``` **事件说明:** - `ready`: 当 SDK 初始化成功时触发,表示收银台已准备就绪。`event.detail` 包含 SDK 实例信息 - `error`: 当 SDK 初始化失败时触发,如 accessToken 无效、网络错误等。`event.detail` 包含 `{ message, code }` 错误信息 ::: warning 注意 事件监听器必须在 `setAttribute` 之前添加,否则可能无法捕获事件。 ```js:line-numbers title="src/config/payment.js" // ❌ 错误:先设置属性再添加监听器 ppCheckout.setAttribute('accessToken', token); ppCheckout.addEventListener('ready', handler); // 可能无法触发! // [!code error] // ✅ 正确:先添加监听器再设置属性 ppCheckout.addEventListener('ready', handler); // [!code focus] ppCheckout.setAttribute('accessToken', token); ``` ::: ## 使用示例 **原生 JavaScript 完整示例** 以下示例展示了如何在原生 JavaScript 项目中集成 SDK,包含完整的项目结构、API 调用和错误处理。 ::: code-tree title="原生 JavaScript 集成示例" height="600px" entry="index.html" ```html:line-numbers title="index.html" :active PingPong Checkout SDK - 原生 JS 示例

PingPong 支付收银台

正在初始化收银台...

``` ```css:line-numbers title="styles.css" * { margin: 0; padding: 0; box-sizing: border-box; } body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif; background: #f5f5f5; padding: 20px; } .container { max-width: 1200px; margin: 0 auto; background: white; padding: 30px; border-radius: 8px; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1); } h1 { color: #333; margin-bottom: 30px; text-align: center; } .loading { text-align: center; padding: 40px; } .spinner { width: 40px; height: 40px; margin: 0 auto 20px; border: 4px solid #f3f3f3; border-top: 4px solid #1890ff; border-radius: 50%; animation: spin 1s linear infinite; } @keyframes spin { 0% { transform: rotate(0deg); } 100% { transform: rotate(360deg); } } .error { padding: 20px; background: #fff2f0; border: 1px solid #ffccc7; border-radius: 4px; color: #ff4d4f; text-align: center; } .error button { margin-top: 15px; padding: 8px 20px; background: #1890ff; color: white; border: none; border-radius: 4px; cursor: pointer; } .error button:hover { background: #40a9ff; } #checkout-wrap { display: none; } ``` ```js:line-numbers title="config.js" // SDK 配置 const CONFIG = { // API 端点配置 apiEndpoint: '/api/reserve', // [!code focus] // SDK CDN 地址(根据环境切换) sdkUrl: { sandbox: 'https://payssr-cdn.pingpongx.com/production-fra/acquirer-checkout-web/sandbox/pp-checkout.js', production: 'https://payssr-cdn.pingpongx.com/production-fra/acquirer-checkout-web/pp-checkout.js' }, // 默认语言 defaultLocale: 'zh', // [!code focus] // 请求超时时间(毫秒) timeout: 30000, // 最小库存数量(用于演示 beforeCheckoutHook) minInventory: 1 }; ``` ```js:line-numbers title="api.js" // API 调用封装 const API = { /** * 获取 AccessToken * @returns {Promise} AccessToken */ async getAccessToken() { // [!code focus] try { const response = await fetch(CONFIG.apiEndpoint, { // [!code focus] method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ // 订单信息 amount: 100.00, currency: 'USD', // ... 其他必要参数 }) }); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); // [!code warning] } const data = await response.json(); if (!data.accessToken) { throw new Error('响应中缺少 accessToken'); // [!code error] } return data.accessToken; // [!code focus] } catch (error) { console.error('获取 AccessToken 失败:', error); throw error; } }, /** * 检查库存(示例) * @returns {Promise<{inventoryQuantity: number}>} */ async checkInventory() { // 模拟 API 调用 return new Promise((resolve) => { setTimeout(() => { resolve({ inventoryQuantity: 10 }); }, 500); }); } }; ``` ```js:line-numbers title="main.js" // 页面加载完成后初始化 document.addEventListener('DOMContentLoaded', async () => { await initCheckout(); // [!code focus] }); /** * 初始化收银台 */ async function initCheckout() { const loadingEl = document.getElementById('loading'); const errorEl = document.getElementById('error'); const errorMessageEl = document.getElementById('error-message'); const checkoutWrap = document.getElementById('checkout-wrap'); const ppCheckout = document.querySelector('pp-checkout'); try { // 1. 等待 SDK 加载完成 await waitForSDK(); // [!code focus] // 2. 配置 SDK hooks setupHooks(); // [!code focus] // 3. 获取 AccessToken const accessToken = await API.getAccessToken(); // [!code focus] // 4. 设置 AccessToken 到 SDK ppCheckout.setAttribute('accessToken', accessToken); // [!code focus] ppCheckout.setAttribute('locale', CONFIG.defaultLocale); // 5. 显示收银台 loadingEl.style.display = 'none'; checkoutWrap.style.display = 'block'; console.log('收银台初始化成功'); } catch (error) { console.error('初始化失败:', error); loadingEl.style.display = 'none'; errorMessageEl.textContent = `初始化失败: ${error.message}`; errorEl.style.display = 'block'; } } /** * 等待 SDK 加载完成 */ function waitForSDK() { return new Promise((resolve, reject) => { const timeout = setTimeout(() => { reject(new Error('SDK 加载超时')); }, CONFIG.timeout); const checkSDK = () => { if (window.PingPong && window.PingPong.Checkout) { // [!code focus] clearTimeout(timeout); resolve(); } else { setTimeout(checkSDK, 100); } }; checkSDK(); }); } /** * 配置 SDK Hooks */ function setupHooks() { // 支付前钩子:检查库存 PingPong.Checkout.beforeCheckoutHook = async () => { // [!code focus] try { const { inventoryQuantity } = await API.checkInventory(); if (inventoryQuantity < CONFIG.minInventory) { throw new Error('库存不足,无法完成支付'); // [!code error] } console.log('库存检查通过,库存数量:', inventoryQuantity); } catch (error) { console.error('库存检查失败:', error); throw error; } }; // 支付结果钩子:接管支付完成后的页面展示或跳转 PingPong.Checkout.checkoutResultHook = async (payload) => { // [!code focus] const orderNo = encodeURIComponent(payload.merchantTransactionId || ''); if (payload.status === 'success') { window.location.href = `/payment/success?orderNo=${orderNo}`; return; } if (payload.status === 'pending') { window.location.href = `/payment/pending?orderNo=${orderNo}`; return; } window.location.href = `/payment/failed?code=${encodeURIComponent(payload.code)}`; }; } ``` ::: **Vue 3 完整示例** 以下示例展示了如何在 Vue 3 项目中集成 SDK,使用 Composition API 实现响应式状态管理。 ::: code-tree title="Vue 3 集成示例" height="600px" entry="App.vue" ```vue:line-numbers title="App.vue" :active