# Widget 接入指南
商户前端页面通过引入我们的 JavaScript SDK,使用 widgetToken 初始化 Token 组件,唤起收银台并完成支付。
# 接入流程
三步接入
- 后端创建订单:调用统一下单接口,传入
widget: true,获取widgetToken - 前端引入 SDK:在商户页面中引入 SDK 脚本
- 前端唤起组件:使用
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 | 用户偏好信息变化 | 通常无需特殊处理 |
# 支付结果处理流程
- 前端收到
RESULT事件 - 前端提示用户"支付已完成"或"正在确认"
- 前端请求商户后端查询订单状态(关键步骤)
- 商户后端以服务端结果为准,返回最终业务状态
- 前端根据后端返回展示成功、失败或处理中页面
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 事件和超时。超时后提示用户稍后查看,或请求后端查询状态。