# Widget 接入指南

商户前端页面通过引入我们的 JavaScript SDK,使用 widgetToken 初始化 Token 组件,唤起收银台并完成支付。

# 接入流程

三步接入

  1. 后端创建订单:调用统一下单接口,传入 widget: true,获取 widgetToken
  2. 前端引入 SDK:在商户页面中引入 SDK 脚本
  3. 前端唤起组件:使用 widgetToken 初始化并唤起收银台,监听支付结果

# 准备工作

  • 商户号(mchNo)和应用ID(appId)已在支付网关注册
  • 后端已完成统一下单接口对接
  • 测试环境和生产环境的域名

# 步骤一:后端获取 widgetToken

商户后端通过 统一订单创建收银台订单创建 接口调用时,传入 widget: true 参数,支付网关返回 widgetToken

重要

  • widgetToken 必须由后端获取,前端不应自行生成
  • 每次支付建议使用新的 widgetToken
  • widgetToken 短期有效,绑定单笔支付

# 请求示例

在统一下单或收银台下单接口中加入 widget: true

{
  "mchNo": "M1623984572",
  "appId": "60cc09bce4b0f1c0b83761c9",
  "mchOrderNo": "mho1624005107281",
  "amount": 100,
  "currency": "BRL",
  "country": "BR",
  "widget": true
}

# 返回示例

返回格式与统一下单/收银台下单接口一致,data 中新增 widgetToken 字段:

{
  "code": 0,
  "data": {
    "payOrderId": "P202106181642329900002",
    "mchOrderNo": "mho1624005107281",
    "orderState": 0,
    "widgetToken": "wt_abc123xyz"
  },
  "msg": "SUCCESS"
}

商户后端将 widgetToken 返回给前端,前端仅使用此 Token 唤起收银台,无需知道其他内部参数。

# 步骤二:引入 SDK

在商户页面中通过 <script> 标签引入 SDK:

# 生产环境

<script type="module" src="https://cashier-hub.enjoypayment.com/sdk/v1/cashier.js"></script>

# 测试环境

<script type="module" src="https://cashier-hub.sandbox.enjoypayment.com/sdk/v1/cashier.js"></script>

# 步骤三:唤起收银台

用户点击支付按钮时调用:

document.getElementById('pay-btn').addEventListener('click', async function () {
  try {
    // 此处 widgetToken 应由后端提供
    const widgetToken = 'wt_abc123xyz';

    const result = await cashier.open({
      widgetToken: widgetToken,
      mode: 'popup' //弹窗模式
      // embed 模式示例:
      // mode: 'embed', //嵌入元素iframe中显示
      // container: '#cashier-container',
      // height: '720px'
    });
    console.log('收银台完成:', result);
  } catch (error) {
    console.error('收银台异常:', error.payload || error);
  }
});

注意事项

  • 必须在用户点击事件中调用 open,避免浏览器拦截弹窗
  • 每次支付建议使用新的 widgetToken
  • 防重复提交:支付按钮点击后置为 loading 状态
  • 不要将内部订单参数暴露到第三方页面

# 事件监听与结果处理

# 监听事件

SDK 通过 cashier.on(eventName, handler) 监听事件:

事件名 说明 建议处理方式
READY SDK 加载完成 关闭商户页面的 Loading 状态
RESULT 支付结果 重点处理。作为支付终态依据,但需后端二次确认
ERROR 加载或支付出错 展示明确提示,允许用户重试
CLOSE 收银台请求关闭 根据业务逻辑处理
REDIRECT 即将跳转至外部页面/渠道 根据业务逻辑处理
STATE_CHANGE 用户偏好信息变化 通常无需特殊处理

# 支付结果处理流程

  1. 前端收到 RESULT 事件
  2. 前端提示用户"支付已完成"或"正在确认"
  3. 前端请求商户后端查询订单状态(关键步骤)
  4. 商户后端以服务端结果为准,返回最终业务状态
  5. 前端根据后端返回展示成功、失败或处理中页面
cashier.on('RESULT', async function (payload) {
  console.log('支付结果:', payload);

  // 必须向后端确认最终状态
  const orderStatus = await fetch(`/merchant-api/order/status?mchOrderNo=${encodeURIComponent(mchOrderNo)}`)
    .then(function (res) { return res.json(); });

  if (orderStatus.status === 'SUCCESS') {
    window.location.href = '/payment-success';
  } else {
    window.location.href = '/payment-processing';
  }
});

安全警告

严禁仅凭前端回调完成发货、开通会员或发放虚拟商品,必须以服务端结果为准

# 完整示例

以下为包含完整交互逻辑的 HTML/JS 示例:

<!doctype html>
<html lang="zh-CN">
<head>
    <meta charset="utf-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <title>Widget 接入示例</title>
    <style>
        body {
            margin: 0;
            min-height: 100vh;
            display: flex;
            align-items: center;
            justify-content: center;
            font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
            background: #f6f8fb;
            color: #172033;
        }

        .page {
            width: min(1040px, calc(100vw - 32px));
            display: grid;
            grid-template-columns: 420px minmax(0, 1fr);
            gap: 20px;
            align-items: start;
        }

        .panel {
            padding: 28px;
            border: 1px solid #d9e2ec;
            border-radius: 16px;
            background: #fff;
            box-shadow: 0 16px 48px rgba(16, 24, 40, 0.07);
        }

        h1 {
            margin: 0 0 12px;
            font-size: 22px;
            line-height: 1.3;
        }

        p {
            margin: 0 0 20px;
            color: #667085;
            line-height: 1.6;
        }

        button {
            width: 100%;
            height: 48px;
            border: 0;
            border-radius: 999px;
            background: #2e7dff;
            color: #fff;
            font-size: 16px;
            font-weight: 600;
            cursor: pointer;
        }

        button:disabled {
            cursor: not-allowed;
            opacity: 0.65;
        }

        .embed-panel {
            min-height: 720px;
            padding: 0;
            overflow: hidden;
        }

        .cashier-container {
            width: 100%;
            min-height: 720px;
            background: #fff;
        }

        .cashier-empty {
            min-height: 720px;
            display: flex;
            align-items: center;
            justify-content: center;
            padding: 24px;
            color: #98a2b3;
            text-align: center;
        }

        @media (max-width: 900px) {
            body {
                align-items: flex-start;
                padding: 16px 0;
            }

            .page {
                grid-template-columns: 1fr;
            }
        }
    </style>
</head>
<body>
<main class="page">
    <section class="panel">
        <h1>Widget 接入示例</h1>
        <p>点击按钮后向商户后端创建订单,拿到 widgetToken 后唤起收银台。默认使用 popup 模式,也可以按注释切换为 embed 模式。</p>
        <button id="pay-btn">立即支付</button>
    </section>

    <section class="panel embed-panel">
        <div id="cashier-container" class="cashier-container">
            <div class="cashier-empty">收银台 iframe 会显示在这里</div>
        </div>
    </section>
</main>

<!-- 生产环境引入 -->
<script src="https://cashier-hub.enjoypayment.com/sdk/v1/cashier.js"></script>
<!-- 测试环境引入:https://cashier-hub.sandbox.enjoypayment.com/sdk/v1/cashier.js -->
<script>
    window.cashier = new EnjoyCashier({
        cashierOrigin: 'https://cashier-hub.enjoypayment.com',
        widgetVersion: 'v1',
        mode: 'popup',
        // embed 模式:收银台会以内嵌 iframe 的方式渲染到指定容器中。
        // 如需切换为 embed,可改为 mode: 'embed',并在 open 时传入 container / element / target。
        // mode: 'embed'
    });

    window.cashier.bootstrap();

    window.cashier.on('READY', function () {
        console.log('Widget 加载完成');
    });

    window.cashier.on('REDIRECT', function (payload) {
        console.log('即将跳转支付页面:', payload);
    });

    window.cashier.on('RESULT', async function (payload) {
        console.log('支付结果:', payload);

        const res = await fetch('/merchant-api/order/confirm');
        const data = await res.json();
        if (data.success) {
            window.location.href = '/success';
        }
    });

    window.cashier.on('CLOSE', function (payload) {
        console.log('用户关闭 Widget:', payload);
    });

    window.cashier.on('ERROR', function (payload) {
        console.error('支付异常:', payload);
        alert('支付失败,请稍后重试');
    });

    document.getElementById('pay-btn').addEventListener('click', async function () {
        const btn = this;
        btn.disabled = true;

        try {
            const res = await fetch('/merchant-api/payment/create', {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    mchOrderNo: 'ORD_' + Date.now(),
                    amount: 100,
                    currency: 'USD',
                    widget: true
                })
            });
            const data = await res.json();

            await window.cashier.open({
                widgetToken: data.widgetToken,
                // embed 模式示例:
                // mode: 'embed',
                // container: '#cashier-container',
                // height: '720px'
            });
        } catch (error) {
            console.error(error.payload || error);
            alert('无法唤起收银台,请稍后重试');
        } finally {
            btn.disabled = false;
        }
    });
</script>
</body>
</html>

# 常见问题

Q: widgetToken 从哪里来?
A: 后端调用统一下单接口收银台订单创建接口时传入 widget: true,返回的 widgetToken 由支付网关生成。

Q: 可以复用同一个 widgetToken 吗?
A: 不建议。每次支付应获取新的 Token。

Q: 支付成功后能直接以前端 RESULT 发货吗?
A: 不可以。前端结果仅用于用户体验,最终订单状态以服务端查询或通知为准。

Q: popup 打不开怎么办?
A: 确认是否在用户点击事件中调用。若仍失败,改用 redirect 模式。

Q: 商户前端需要知道内部支付参数吗?
A: 不需要。前端只需要 widgetToken

Q: 收银台无响应怎么办?
A: 处理 ERROR 事件和超时。超时后提示用户稍后查看,或请求后端查询状态。

Last Updated: 2026/6/15 下午2:49:10