SDK API 参考
本文是 Session Control 客户端 SDK 的完整 API 手册。目标:你只读这一篇(配合快速开始的环境准备),就能把 SDK 100% 接入成功。
阅读顺序建议:
- 接入前置条件 —— 不满足就开工是接入失败的第一大原因
- 加载方式与全局对象
- 会话生命周期 —— 理解状态机,后面每个方法都围绕它转
- 方法总览 → 逐个方法详解
- 回调与命令 payload(自绘 UI 必读)
- 错误处理(排错必读)
- 接入验收清单(接完照着打勾)
接入前置条件
以下条件缺一不可,任何一个不满足都会导致接入"看起来没生效":
| # | 前置条件 | 不满足时的现象 |
|---|---|---|
| 1 | 平台(服务端 + 管理端)已部署并运行,MySQL / Redis 已就绪 | POST /init 网络失败或 502 |
| 2 | 已用运营方发放的操作员账号登录管理端,创建 Site 并复制 API Key(sk_ 开头) | POST /init 返回 401「无效的站点 API Key」 |
| 3 | 商城页面域名已加入管理端域名白名单(本地联调加 localhost、127.0.0.1) | POST /init 返回 403「来源域名不在租户白名单内」 |
| 4 | 租户全局服务开关处于开启状态 | ① SDK 脚本分发返回空脚本(window.SessionControl 不存在);② 已缓存 SDK 的上报全部 403「租户已关闭会话控制」 |
| 5 | 页面域与平台同源:页面域下的 /api/* 必须能路由到平台(平台同域部署,或商城网关反代 /api/* 到平台) | 上报 404(Cannot POST /session/init,多为反代剥前缀)或跨域失败 |
前置条件 1–4 的操作步骤见快速开始与租户端配置;条件 5 的反代配置示例(Nuxt / Vite / Next.js / Nginx)见SDK 使用方法。
最快验证环境是否就绪:浏览器直接访问
https://{你的页面域}/api/sdk/{siteKey}.js——应返回一段 JS 脚本;再访问https://{你的页面域}/api/shopper/session/init(POST,带任意 JSON body)——应返回 JSON 而非 404/403/401。
加载方式与全局对象
script 标签(免构建,平台分发,推荐)
<script src="https://{platform-domain}/api/sdk/{site_api_key}.js"></script>
<!-- 加载完成后安全调用(全局开关关闭时脚本为空,需判断避免 TypeError) -->
<script>
if (window.SessionControl) {
window.SessionControl.init({ siteKey: '{site_api_key}' })
}
</script>{platform-domain}:承载商城页面的那个域(SDK 与页面同源,见前置条件 5),不是"平台管理端的域"——如果商城页和平台不同域,这里填商城域,由商城网关把/api/*反代到平台{site_api_key}:管理端创建 Site 时复制的sk_开头的 Key
npm(模块化构建)
import { SessionControl, type InitConfig } from '@session/sdk'
@session/sdk当前private: true,未发布到公共 registry,仅在本仓库 workspace 内可用;外部商城请用 script 标签方式。
两种方式暴露同一个单例
两种方式暴露同一个 SessionControl 单例对象(SessionControlCore 类的实例),内部状态共享(指纹缓存、防抖定时器、轮询定时器等)。
- 使用方只操作
SessionControl单例即可,不要new SessionControlCore()(SessionControlCore导出仅供测试/特殊多实例场景;多实例各自独立轮询与上报,正常接入用不到)
会话生命周期
SDK 的一切行为都围绕"会话"展开。会话 = 一个正在逛商城的购物者(按浏览器指纹标识,存在 localStorage,跨刷新/标签页复用),存于服务端 Redis,TTL 60s(任何一次上报都会续期)。
reportFormData / reportCart verify()
TRACKING ───────────────────────────► CHECKOUT ──────────► PENDING_REVIEW
(浏览中) (填了结算表单/有了订单号) (已提交订单,等待人工验证)
▲ │ │
│ │ │
└─────────────────────────────────────────────────────┘ │
管理端「完成订单」归档 或 停止上报超时 │ 停止上报 >50s
▼
COMPLETED(归档为订单) / ABANDONED(放弃归档)| 状态 | 含义 | 商城侧表现 |
|---|---|---|
TRACKING | 会话已建立,购物者在浏览 | 无弹窗;管理端无法下发指令 |
CHECKOUT | 已上报表单或购物车 | 无弹窗;管理端可下发指令 |
PENDING_REVIEW | 已调 verify(),等待人工验证 | 弹窗接管(等待视图或验证视图),SDK 开始 3s 轮询指令 |
COMPLETED | 管理端下发「完成订单」,归档为历史订单 | 弹窗关闭,回调 onComplete,SDK 自动开启新一轮会话 |
ABANDONED | 进入结算后停止上报超 50s,自动放弃归档 | 弹窗关闭(会话已重建为 TRACKING) |
购物者停止上报 1 分钟(TTL 60s),Redis 会话键过期消失。验证弹窗期间 SDK 只轮询
pending,轮询本身就会续期,所以弹窗开着不会过期。
方法总览
SDK 暴露一个单例对象 SessionControl,共 7 个公开方法 + 6 个类型导出:
| 方法 | 作用 | 必调 | 调用时机 | 不调用的后果 | 同步/异步 |
|---|---|---|---|---|---|
init(config) | 初始化:生成指纹、建立会话、自动上报当前页面 | ✅ | 页面加载完成时,await 后再调其他方法 | 其他方法全部失效(上报被忽略、verify() 返回 not_ready) | async |
reportPageView(url, pageName?) | 手动上报页面(SPA 路由变更时) | 视情况 | SDK 无法自动感知的跳转(location.href、iframe 内跳转) | 管理端看不到页面变化(SDK 已自动监听 pushState/popstate/hashchange,绝大多数场景无需调用) | sync |
reportFormData(data) | 上报表单数据(SDK 内置防抖,change 事件直接调) | ✅ | 表单 input/change 事件中 | verify() 返回 400(credit_card 无卡号 / paypal 无邮箱),无法进入人工验证 | sync |
reportCart(orderNo, items) | 上报订单号与购物车 | 视情况 | 配了 orderProvider 时由 SDK 自动调用;未配则自行调用 | 管理端无订单号、无购物车弹窗内容 | sync |
verify() | 提交订单,进入人工验证流程 | ✅ | 用户点击「提交订单」时 | 会话永远停在 CHECKOUT,管理端无法接管验证 | async |
submitVerification(commandId, value) | 提交验证码/整值 | 视情况 | 自绘验证 UI 时;内置弹窗自动调用 | 内置弹窗可用时无需关心;自绘 UI 不调用则验证永远无法通过 | async |
destroy() | 销毁:停止上报与轮询 | 可选 | SPA 离开结算页/组件卸载时 | 组件反复挂载会累积事件监听与弹窗 DOM(SDK 内部做了防重复,但规范做法是配对调用) | sync |
类型导出(npm 方式):
InitConfig、FormDataPayload、CartItem、Address、ActiveCommand、SDKError。最小可用接入只需 3 个调用:
init()+reportFormData()+verify()。配了orderProvider/formProvider后,连reportCart和"手动上报表单的时机"都不用操心,SDK 在verify()前自动补齐。
init(config)
作用:SDK 初始化。做三件事,全部成功后 SDK 进入就绪态:
- 生成/复用浏览器指纹:优先读 localStorage 缓存的指纹(跨刷新/标签页稳定);无缓存时用 FingerprintJS 计算(内部预热两次,取稳定值);FingerprintJS 不可用(隐私模式/加载失败)时降级为随机 ID(也会缓存,避免每次刷新都生成新指纹导致多会话)
- 建立会话:
POST /api/shopper/session/init(body 携带siteKey + fingerprint + userAgent + 当前 URL + pageName)。服务端按指纹命中/创建会话并续期 - 自动上报当前页面 + 安装 SPA 路由监听(
pushState/replaceState/popstate/hashchange)+ 恢复上次会话状态(若上次是 PENDING_REVIEW,重新拉起等待/验证弹窗并启动轮询——刷新页面不丢验证流程)
init(config: InitConfig): Promise<void>参数:InitConfig
interface InitConfig {
siteKey: string
pageName?: string
merchantName?: string
orderProvider?: () => Promise<{ orderNo: string; cartItems?: CartItem[] } | string>
formProvider?: () => FormDataPayload | Promise<FormDataPayload>
formDebounceMs?: number
onError?: (error: SDKError) => void
onCommand?: (command: ActiveCommand) => void
onComplete?: (result: { orderNo?: string }) => void
}| 参数 | 类型 | 必填 | 默认 | 说明 |
|---|---|---|---|---|
siteKey | string | ✅ | — | Site 级 API Key(sk_ 开头,管理端创建 Site 时复制) |
pageName | string | 否 | — | 当前页面名称(如"结算页"),随 init 上报,展示在管理端会话卡片 |
merchantName | string | 否 | — | 商户名(如 "Demo Mall"),验证弹窗交易详情条展示("Merchant: xxx") |
orderProvider | () => Promise<{orderNo, cartItems?} | string> | 否 | — | 订单号提供者。首次 verify() 前 SDK 自动调用它建单。订单号必须由租户服务端真实生成(SDK 不自行生成)。详见下文 |
formProvider | () => FormDataPayload | Promise<FormDataPayload> | 否 | — | 表单兜底:全程未上报过表单时,verify() 主动调它拉一次并上报。返回结构同 reportFormData 参数 |
formDebounceMs | number | 否 | 500 | reportFormData 内置防抖毫秒数 |
onError | (error: SDKError) => void | 否 | — | 内部错误回调,见错误处理 |
onCommand | (command: ActiveCommand) => void | 否 | — | 管理端下发验证命令时回调(SDK 已内置弹窗,此回调用于自绘 UI 或埋点),见命令 payload |
onComplete | (result: {orderNo?}) => void | 否 | — | 验证全部完成后回调,result.orderNo 为本轮完成的订单号。触发条件见下文"边界与陷阱" |
orderProvider 详解(接入方最容易出错的地方)
职责:返回商城真实订单号(+ 购物车明细),由 SDK 在首次 verify() 前调用,用于:
- 服务端把订单号/购物车与会话关联(管理端订单核验、购物车弹窗展示)
- 若只返回
{ orderNo }不带cartItems,管理端购物车弹窗会出现「订单号有、商品为空」
铁律:
- ❌ 严禁返回前端临时 UUID——管理端订单号必须与商城后端订单号 100% 一致,否则操作员无法核验、完成订单后商城也找不到对应订单
- ✅ 必须调用商城后端真实建单接口获取订单号(即使订单尚未支付,先占号):typescript
orderProvider: () => fetch('/order/create', { method: 'POST' }) // 商城自己的建单接口 .then(r => r.json() as Promise<{ orderNo: string; cartItems: CartItem[] }>) - ⚠️ 建单接口不要用
/api/前缀(/api/*已被平台反代占用,示例用/order/create) - ⚠️ 返回值两种形态:
string(只有订单号)或{ orderNo, cartItems? }(推荐,带上商品明细) - 调用失败(抛异常/返回空)时 SDK 静默降级:
verify()继续走,只是没有订单号;下次verify()会重试建单 - 连点提交订单安全:并发闸门保证同一趟建单请求只发一次,租户服务端只建一单(不会产生孤儿订单)
返回值
Promise<void>——没有返回值。成功与否要看副作用:
- 成功:
window.SessionControl就绪,后续方法生效 - 失败(无效 apiKey / 白名单拒绝 / 网络错误):静默失败不抛异常,SDK 不进入就绪态;若配置了
onError会收到回调
⚠️ 边界与陷阱
- 必须
await:init是 async,未完成时调用其他方法会被静默忽略(页面/表单上报不发送),verify()返回'not_ready'。页面加载完成后立即await init()再挂表单监听 - 可重复调用:
init()可安全调用多次(幂等);destroy()之后也可重新init()(复用同一指纹,不重新生成) - SSR 安全:SDK 在非浏览器环境(服务端渲染)不会崩溃,但不会有任何效果——务必在客户端生命周期钩子中调用(Vue:
onMounted,React:useEffect) pageName/merchantName只在init时设置,之后再次调用init会整体替换配置
await SessionControl.init({
siteKey: 'sk_live_abc123',
pageName: '结算页',
merchantName: 'Demo Mall',
// 订单号由租户服务端生成;cartItems 决定管理端购物车弹窗内容
orderProvider: () =>
fetch('/order/create', { method: 'POST' })
.then(r => r.json() as Promise<{ orderNo: string; cartItems: CartItem[] }>),
// 全程没上报过表单就点了提交时,SDK 用它兜底拉一次
formProvider: () => currentCheckoutForm(),
onError: (error) => {
console.error('[SessionControl]', error.code, error.message)
},
onComplete: (result) => {
console.log('订单完成', result.orderNo) // 本轮完成的订单号
location.href = '/checkout/done'
}
})reportPageView(url, pageName?)
作用:手动上报页面。SDK 在 init() 时已自动上报当前页面,并自动监听 history.pushState/replaceState、popstate、hashchange 感知 SPA 路由变化;此方法用于 SDK 无法自动感知的跳转场景:
location.href = '...'直接赋值(不经过 history API)- iframe 内跳转
- 其他非标准路由库
reportPageView(url: string, pageName?: string): void| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
url | string | ✅ | 当前页面完整 URL(window.location.href) |
pageName | string | 否 | 页面名称(如 "商品页"),展示在管理端会话卡片 |
行为细节:
- 同步方法,发出
POST /api/shopper/session/page(异步发送,不阻塞调用方) - 调用后更新 SDK 内部“最近上报 URL”记录,后续自动监听路径(
pushState/popstate/hashchange触发)发现 URL 与上次相同则跳过——防止pushState与随后的popstate重复上报 - SDK 未就绪 / 已销毁时调用被静默忽略;上报失败静默(不中断用户流程)
// 例如在 Vue Router 的 afterEach 钩子中
router.afterEach((to) => {
SessionControl.reportPageView(window.location.href, to.name as string)
})reportFormData(data)
作用:上报结算表单数据。服务端按指纹关联到同一会话,管理端据此核对支付凭据(卡号/金额/地址等),并作为 verify() 的表单完整性校验输入。
核心设计:内置防抖(formDebounceMs,默认 500ms)。表单 input/change 事件里直接调用即可:
- 本地状态(弹窗卡号/金额展示)实时更新
- 网络请求合并为"最后一次变更后"上报(连续输入只发一次)
verify()前强制 flush(清掉防抖定时器、立即上报),保证最新表单先到服务端再进验证流程
reportFormData(data: FormDataPayload): void参数:FormDataPayload
interface FormDataPayload {
paymentMethod?: 'credit_card' | 'paypal'
cardNumber?: string
cardHolder?: string
expiryDate?: string
cvv?: string
billingAddress?: Address
paypalEmail?: string
paypalAccount?: string
shippingAddress?: Address
/** 订单金额,推荐格式如 "$159.00"、"¥2,228.00" 或数值 159.00(请勿添加多余英文字母后缀如 USD/CNY) */
amount?: string
/** 总金额,同 amount */
total?: string
/** 订单总额,同 amount */
orderTotal?: string
// 允许扩展字段,服务端原样存储
[key: string]: unknown
}
interface Address {
line1: string
line2?: string
city: string
state?: string
postalCode: string
country: string
}| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
paymentMethod | 'credit_card' | 'paypal' | ✅* | 支付方式。*verify() 校验依赖它:未指定时按 credit_card 处理 |
cardNumber | string | credit_card 必填 | 卡号。不能全为空格,否则 verify() 返回 400 |
cardHolder | string | 否 | 持卡人姓名(管理端核对用) |
expiryDate | string | 否 | 有效期,如 12/25 |
cvv | string | 否 | CVV。⚠️ 服务端存储用于管理端核验;pending 轮询回读时服务端会剔除卡号/CVV,不会下发到商城链路 |
billingAddress | Address | 否 | 账单地址 |
paypalEmail | string | paypal 必填 | PayPal 邮箱,不能为空,否则 verify() 返回 400 |
paypalAccount | string | 否 | PayPal 账号 |
shippingAddress | Address | 否 | 收货地址 |
amount / total / orderTotal | string | 否 | 订单金额(三选一,SDK 优先取 amount)。必须是字符串,格式见下 |
| 扩展字段 | unknown | 否 | 任意自定义字段,服务端原样存储(管理端表单明细可见) |
⚠️ 金额格式规范(管理端展示依赖)
- ✅ 推荐:货币符号 + 数值字符串,如
"$159.00"、"¥2,228.00";或纯数字字符串"159.00" - ❌ 禁止:带英文币种单词后缀,如
"$159.00 USD"、"159 CNY"——管理端金额解析引擎只过滤货币符号,英文后缀会导致NaN,订单详情金额显示为— - ⚠️ 类型必须是
string:传number时 SDK 弹窗不展示金额(内部只认字符串);请String(amount)或模板字符串
credit_card 示例(change 直接调,防抖交给 SDK):
function onCheckoutFormChange() {
SessionControl.reportFormData({
paymentMethod: 'credit_card',
cardNumber: '4111111111111111',
cardHolder: 'John Doe',
expiryDate: '12/25',
cvv: '123',
amount: `$${total.toFixed(2)}`,
billingAddress: {
line1: '123 Main St',
city: 'New York',
postalCode: '10001',
country: 'US'
}
})
}
document.getElementById('checkout-form').addEventListener('input', onCheckoutFormChange)paypal 示例(表单无卡号,字段为 PayPal 邮箱/账号):
SessionControl.reportFormData({
paymentMethod: 'paypal',
paypalEmail: 'shopper@example.com',
paypalAccount: 'shopper_paypal_id'
})行为细节与边界
- SDK 未就绪 / 已销毁时调用被静默忽略(务必先
await init()) - 数据量上限:序列化后超过 16KB 时服务端返回 400「表单数据过大」(防 API Key 泄露后刷大键);正常表单远达不到
- 卡号会被 SDK 吸收到本地状态:弹窗展示卡品牌标签 + 掩码(前6后4);刷新后恢复靠本地掩码缓存(服务端不下发明文卡号)
- 上报失败静默(
onError可收到回调),不中断用户流程
reportCart(orderNo, items)
作用:上报订单号与购物车,关联到当前指纹会话。管理端「购物车信息」弹窗展示订单号与商品明细;表单未带金额时,SDK 用购物车合计(Σ price × quantity)兜底展示在验证弹窗。
reportCart(orderNo: string, items: CartItem[]): void| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
orderNo | string | ✅ | 商城侧订单号(与商城后端一致) |
items | CartItem[] | ✅ | 购物车商品列表(price × quantity 合计作为金额兜底) |
interface CartItem {
name: string
price: number
quantity?: number
}什么时候需要调用?
一般不需要手动调用:
init配了orderProvider→ SDK 在首次verify()前自动建单并调用本方法上报,之后同一会话不再重发(服务端做 CAS 差量合并,不会丢)- 不配
orderProvider→ 才由接入方在生成订单号后自行调用(此时请在点击提交订单、调verify()之前完成)
注意事项:
items决定管理端购物车弹窗内容:只传orderNo不传items(或cartItems为空数组)时弹窗出现「订单号有、商品为空」- 同一会话内重复调用会覆盖订单号/购物车(幂等场景:重复调用无副作用)
- SDK 未就绪 / 已销毁时调用被静默忽略
SessionControl.reportCart('ORD-20260828-0001', [
{ name: 'Wireless Headphones', price: 199.0, quantity: 1 },
{ name: 'USB-C Cable', price: 9.9, quantity: 2 }
])verify()
作用:商城每次点击提交订单时调用。服务端校验表单后会话进入 PENDING_REVIEW,SDK 开始每 3–4s 轮询 /pending 接收管理端指令,并弹出等待/验证视图。
这是整个接入最核心的方法。调用它意味着:"这个购物者提交订单了,请平台接管后续验证流程"。
调用前的自动补齐(按序执行)
verify() 被调用
│
├─ 1. flush 防抖中的表单数据(等防抖定时器立即生效、发送完成)
├─ 2. 全程未上报过表单?
│ └─► 是 → 调 formProvider() 拉取并上报(等待返回;provider 异常则跳过)
├─ 3. 首次结算?
│ └─► 是 → await orderProvider() 建单(并发闸门:连点共用一趟请求)
│ → 异步发出 /order 上报(不阻塞 verify 请求本身)
├─ 4. 拒卡判重:本地记录的"被拒卡号"与当前卡号一致?
│ └─► 是 → 顶部通知栏展示拒卡文案,直接返回 { action: 'rejected' }(不进验证流程)
├─ 5. POST /api/shopper/session/verify
│ └─► 400(表单缺卡号/邮箱)或网络失败 → 返回 { action: 'error' }
│ └─► 成功 → 进入 PENDING_REVIEW,启动轮询,渲染命令/等待弹窗
└─ 返回 { action }verify(): Promise<{ action: string }>返回值 action 枚举
| action | 含义 | 商城侧表现 | 需要接入方处理? |
|---|---|---|---|
'loading' | 已进入验证流程 | SDK 已弹出等待弹窗(等管理端下发)或直接渲染活跃命令弹窗 | 否(SDK 已接管 UI) |
'rejected' | 拒卡后卡号未更换 | SDK 顶部通知栏展示拒卡文案,不弹窗 | 可引导用户换卡 |
'not_ready' | SDK 未就绪 | 不进入验证流程 | 必须先 await init() 再调用;提示用户稍后重试 |
'error' | 请求被拒/失败(如未上报表单返回 400) | 不弹窗 | 错误经 onError 回调;提示用户重试 |
verify()不会抛出异常(内部网络错误被吞掉转为'error'/'not_ready')。但你的orderProvider/formProvider若抛异常也会被吞掉(建单失败 → 无订单号继续验证)。若要区分"平台未启用"与"业务失败",把商城原生结算逻辑放在action === 'error' || 'not_ready'分支做降级放行即可。
// 用户点击「提交订单」时调用
const { action } = await SessionControl.verify()
switch (action) {
case 'loading':
// SDK 已弹出等待/验证弹窗,无需额外处理
break
case 'rejected':
// 拒卡未换卡:顶部通知栏已展示,可引导用户换卡
break
case 'not_ready':
case 'error':
// 未就绪或失败:提示用户稍后重试(或降级走商城原生结算)
break
}边界与陷阱
- 连点安全:用户连点提交订单,多个
verify()共用同一趟建单请求——租户服务端只建一单、/order只上报一次(否则旧单号会成为租户侧孤儿订单)。完成订单后 SDK 重建会话,闸门同步重置,新一轮结算重新建单 complete_order不通过onCommand回调下发:服务端下发即归档删键,SDK 在轮询发现会话离开 PENDING_REVIEW 时回调onComplete(状态兜底)onComplete有 15s 新鲜度门槛:仅当弹窗开着、且距上次成功轮询不超过 15s 才判定为"订单完成"。页面被挂起超过 15s(典型:App 验证跳去银行 App 再回来)时,会话可能已 TTL 过期重建,此时不会回调onComplete——接入方不要把完成页跳转只挂在这个回调上(建议同时监听自己的订单状态接口)- 管理端未接管也可下发指令;接管只是绑定操作员展示,不影响命令下发
- TRACKING 状态下管理端无法下发指令(
isCommandIssuable限制),所以必须先上报表单再 verify,否则会话停在 TRACKING 时管理端没法操作
submitVerification(commandId, value)
作用:提交验证码/整值到服务端。SDK 内置弹窗会在用户点击确认时自动调用;此方法供商城自绘验证 UI(配合 onCommand 回调)时使用。
submitVerification(commandId: string, value: string): Promise<boolean>| 参数 | 类型 | 必填 | 说明 |
|---|---|---|---|
commandId | string | ✅ | onCommand 回调中 command.commandId(服务端下发时生成,必须原样回传) |
value | string | ✅ | 验证码/整值(长度上限 4096) |
返回值
Promise<boolean>:是否上报成功。
true:服务端已记录(命令置为 completed,管理端已验证列表可见)false:失败(网络/服务端错误,或命令已失效)。内置弹窗会自动恢复输入框与确认按钮允许重试;自绘 UI 时请根据返回值恢复输入,否则用户会卡死
服务端只接受
status === 'active'的命令:无效 commandId 或已完成的命令一律 400(防随意提交污染验证记录)。若操作员已把该命令作废/换新命令,提交会失败。
// 自绘验证 UI 示例(配合 onCommand)
await SessionControl.init({
siteKey: 'sk_live_abc123',
onCommand: async (command) => {
if (command.type === 'otp_verification') {
const code = await prompt('请输入验证码')
if (!code) return
const ok = await SessionControl.submitVerification(command.commandId, code)
if (!ok) alert('提交失败,请重试')
}
}
})destroy()
作用:销毁 SDK 实例。停止上报与指令轮询、卸载 history 补丁与事件监听、清理弹窗 DOM。
destroy(): void行为细节:
- 同步方法,立即生效
- 销毁后可重新
init():复用 localStorage 缓存的同一指纹重建/续期会话(不重新生成指纹) - 所有内部定时器(轮询、防抖)与事件监听全部清理,不产生泄漏
⚠️ 边界与陷阱
destroy()不重置已缓存的订单号与"已上报过表单"标记(只有 complete_order 后的内部重建会重置):同一页面实例 destroy → init 后,verify()不会再次调orderProvider建单- 若原会话已 TTL 过期,服务端新会话没有表单,
verify()会返回 400(需先重新上报表单) - 通常不需要调用:对单页应用,最规范的做法是离开结算页(组件卸载)时调用,返回时重新
init()
// SPA 离开结算流程时
onUnmounted(() => {
SessionControl.destroy()
})回调与命令 payload
onCommand 回调与命令 payload
管理端每下发一条命令,SDK 在渲染弹窗的同时调用 onCommand(cmd)。内置弹窗已自动处理全部 8 种命令,只有自绘 UI / 埋点才需要关心 payload。命令类型与 payload 字段:
命令类型 type | 含义 | payload 关键字段 | 弹窗行为 |
|---|---|---|---|
otp_verification | OTP 短信验证码 | phone?(尾号展示用,如 {phone: '138****8000'} 取后4位) | 输入框(数字键盘) |
phone_verification | 手机号验证码 | phone? | 输入框(数字键盘) |
email_verification | 邮箱验证码 | email?(打码展示,如 q****ctb@iubridge.com) | 输入框 |
app_verification | App 推送验证 | issuerName?(开卡银行名,管理端 BIN 查询自动带出)、amount?(交易金额,来自上报表单金额/购物车合计) | 无输入框,引导去银行 App 确认 |
reject_card | 拒绝信用卡 | message?(拒卡文案,管理端自定义)、rejectedCard(下发时绑定的完整卡号) | 拒卡弹窗(可关闭/换卡) |
reject_paypal | 拒绝 PayPal | 同 reject_card | 拒卡弹窗 |
complete_order | 完成订单 | —(正常路径不经过此回调,见 verify() 陷阱 2) | 关闭弹窗 → onComplete |
redirect_to_loading | 重定向到等待状态 | — | 弹窗切回等待视图 |
ActiveCommand 结构见类型定义。
onError 回调
SDK 内部遇到可恢复/不可恢复的上报失败时触发(网络错误、HTTP 4xx/5xx)。SDKError:
interface SDKError {
code: string // 当前唯一取值 'REQUEST_FAILED'
message: string // 人类可读描述,如 "Request failed (403)"、"Network error, report failed"
}- 网络错误会自动重试一次(1s 后),重试仍失败才回调
- 回调里不要抛异常(会中断 SDK 内部流程);用于埋点/告警/提示即可
- 服务端 4xx 的详细语义见错误处理
onComplete 回调
(result: { orderNo?: string }) => void,触发时机:
管理端下发 complete_order → 服务端同步归档删键 → SDK 轮询发现会话离开 PENDING_REVIEW(15s 新鲜度内)→ 回调。
result.orderNo 为本轮完成订单的订单号(来自 orderProvider 或 reportCart)。回调后 SDK 自动重建新会话(同指纹),商城无需感知,下一轮结算自动接入新会话。
⚠️ 完成页跳转不要只依赖此回调(15s 新鲜度门槛),见
verify()陷阱 3。
类型定义
ActiveCommand
interface ActiveCommand {
commandId: string
type: 'otp_verification' | 'phone_verification' | 'email_verification'
| 'app_verification' | 'reject_card' | 'reject_paypal'
| 'complete_order' | 'redirect_to_loading'
payload: Record<string, unknown>
status: 'active' | 'completed' | 'interrupted'
operatorId?: number
operatorName?: string
issuedAt: number
}| 字段 | 类型 | 说明 |
|---|---|---|
commandId | string | 命令唯一 ID(cmd_ 开头),submitVerification 需原样回传 |
type | string | 命令类型(8 种,见上文 payload 表) |
payload | object | 命令参数(提示文案、手机/邮箱、银行名、金额等,按 type 变化) |
status | 'active' | 'completed' | 'interrupted' | 命令状态:下发为 active;验证提交后 completed;会话中断/重建后作废 |
operatorId? / operatorName? | number / string | 下发操作员 |
issuedAt | number | 下发时间戳(ms) |
payload里字段的具体含义见命令 payload 表。拒卡命令绑定了下发时的卡号:购物者换卡后旧拒卡命令不再返回(服务端按卡号过滤),所以onCommand收到reject_card时针对的一定是当前卡。
SDKError
interface SDKError {
code: string // 当前唯一取值 'REQUEST_FAILED'
message: string
}CartItem / Address / FormDataPayload / InitConfig
见reportCart、reportFormData、init各节定义。
错误处理
SDK 视角(onError / 返回值)
| 场景 | SDK 行为 |
|---|---|
| 网络错误(断网/超时/连接重置) | 自动重试 1 次(1s 后);仍失败 → onError({ code: 'REQUEST_FAILED', message: 'Network error, report failed' });对应方法走失败分支(verify() 返回 'error',submitVerification() 返回 false) |
| HTTP 4xx/5xx(服务端拒绝) | onError({ code: 'REQUEST_FAILED', message: 'Request failed (403)' });不解析错误体 |
| SDK 未就绪(未 init / 已 destroy) | 上报方法静默忽略;verify() 返回 'not_ready';submitVerification() 返回 false |
HTTP 状态码语义(服务端)
| 状态码 | 含义 | 典型触发 | 排查 |
|---|---|---|---|
401 | 无效的站点 API Key | siteKey 错误/站点已删除/站点已停用 | 核对管理端 Site 的 API Key |
403 | 租户已关闭会话控制 | 全局开关关闭 | 管理端开启全局开关 |
403 | 来源域名不在白名单内 | 页面域未加入白名单 | 管理端加入白名单(本地联调加 localhost) |
400 | 表单数据过大(>16KB) | 异常大的表单 | 精简上报字段 |
400 | 请先上报表单数据(卡号/PayPal 邮箱)再提交订单 | verify() 前没上报表单,或表单缺关键字段 | 确保 change 事件上报 / 配 formProvider |
400 | 无效的验证命令或命令不在活跃状态 | submitVerification 传了过期 commandId | 使用最新 onCommand 里的 commandId |
404 | 路径不存在 | 反代剥前缀(/session/init 而非 /api/shopper/session/init) | 检查反代配置,见integration |
服务端拒绝时 SDK 不解析错误体(避免误判完成),统一走
onError+ 失败分支,所以商城侧无法直接读到服务端错误文案——按上表对照状态码排查即可(Network 面板可看到具体状态码)。
浏览器兼容与降级行为
| 能力 | 要求/降级 |
|---|---|
| 浏览器 | 现代浏览器(Chrome/Edge/Firefox/Safari 近两年版本)。需要 fetch、localStorage、history API |
| FingerprintJS 失败(隐私模式/脚本异常) | 降级为随机 ID 并缓存(同浏览器刷新不变;跨浏览器/清缓存会变) |
| localStorage 不可用 | 指纹每次重新计算(会话稳定性下降,不报错) |
| 页面域与平台跨域 | 不支持,必须同源或反代(前置条件 5) |
| CSP | 若页面启用严格 CSP,需放行平台的 script-src(script 方式)与 connect-src(同源反代则天然放行) |
| SSR(服务端渲染) | SDK 内部做了环境判断,服务端调用不崩溃但无效果;务必在客户端钩子中调用 |
完整接入示例
最小 HTML 接入(script 标签)
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Demo Mall - 结算</title>
</head>
<body>
<h1>结算页</h1>
<form id="checkout-form">
<input id="card-number" placeholder="卡号" autocomplete="cc-number">
<input id="card-holder" placeholder="持卡人" autocomplete="cc-name">
<input id="expiry" placeholder="有效期 MM/YY" autocomplete="cc-exp">
<input id="cvv" placeholder="CVV" autocomplete="cc-csc" type="password">
<button id="submit-btn" type="submit">提交订单</button>
</form>
<!-- 1. 引入 SDK(与页面同源,自动暴露 window.SessionControl) -->
<script src="https://{platform-domain}/api/sdk/{your_site_api_key}.js"></script>
<!-- 2. 顶层 await 只在 module 脚本里合法;经典 script 请改用 .then() -->
<script type="module">
// 3. 初始化:订单号/表单由租户服务端或页面状态提供,SDK 在 verify 前自动取用
await SessionControl.init({
siteKey: '{your_site_api_key}',
merchantName: 'Demo Mall',
// 订单号必须由租户服务端生成;返回 { orderNo, cartItems } 才能带上购物车
orderProvider: () => fetch('/order/create', { method: 'POST' }).then(r => r.json()),
formProvider: collectForm,
onComplete: () => { location.href = '/checkout/done' },
onError: (e) => console.warn('[SessionControl]', e.code, e.message)
})
// 4. 表单变化直接上报,SDK 内置 500ms 防抖合并连续变更
function collectForm() {
return {
paymentMethod: 'credit_card',
cardNumber: document.getElementById('card-number').value,
cardHolder: document.getElementById('card-holder').value,
expiryDate: document.getElementById('expiry').value,
cvv: document.getElementById('cvv').value
}
}
document.getElementById('checkout-form').addEventListener('input', () => {
SessionControl.reportFormData(collectForm())
})
// 5. 提交订单:建单、购物车上报、表单兜底都由 verify 内部完成
document.getElementById('checkout-form').addEventListener('submit', async (e) => {
e.preventDefault()
await SessionControl.verify() // 弹窗/通知栏由 SDK 内置处理
})
</script>
</body>
</html>Vue 3 接入(npm 包)
<script setup lang="ts">
import { onMounted, onUnmounted, reactive, watch } from 'vue'
import { ElMessage } from 'element-plus'
import { SessionControl } from '@session/sdk'
const checkoutForm = reactive({ cardNumber: '', cardHolder: '', expiryDate: '', cvv: '' })
onMounted(async () => {
await SessionControl.init({
siteKey: 'sk_live_abc123',
pageName: '结算页',
merchantName: 'Demo Mall',
// 订单号由租户服务端生成,cartItems 决定管理端购物车弹窗内容
orderProvider: () =>
fetch('/order/create', { method: 'POST' })
.then(r => r.json() as Promise<{ orderNo: string; cartItems: CartItem[] }>),
formProvider: () => ({ paymentMethod: 'credit_card', ...checkoutForm }),
onComplete: () => { /* 跳转完成页 */ }
})
})
// 表单变化直接上报,SDK 内置防抖(formDebounceMs,默认 500ms)
watch(checkoutForm, () => SessionControl.reportFormData({ paymentMethod: 'credit_card', ...checkoutForm }))
async function submitOrder() {
// 建单/购物车上报/表单兜底都在 verify 内部完成,连点也只建单一次
const { action } = await SessionControl.verify()
if (action === 'error' || action === 'not_ready') {
ElMessage.error('提交失败,请重试')
}
}
onUnmounted(() => SessionControl.destroy())
</script>常见场景速查
| 场景 | 调用 |
|---|---|
| 页面加载 | await SessionControl.init(config) |
| SPA 路由切换 | 依赖 SDK 自动监听;特殊情况(location.href 等)SessionControl.reportPageView(url, name?) |
| 表单填写完成/change | SessionControl.reportFormData(data)(SDK 内置防抖,直接调) |
| 首次结算生成订单号 | init 里配 orderProvider,SDK 在首次 verify() 前自动建单并上报 |
未配 orderProvider 时手动上报订单 | SessionControl.reportCart(orderNo, items)(在 verify() 之前) |
| 点击提交订单 | await SessionControl.verify()(连点不会重复建单) |
| 自绘验证 UI 收码 | await SessionControl.submitVerification(commandId, value) |
| 离开结算页 | SessionControl.destroy() |
接入验收清单
接入完成后,按顺序验证(对应快速开始的完整链路):
- [ ] ① 环境就绪:浏览器访问
/{你的域}/api/sdk/{siteKey}.js返回 JS 脚本(非空);POST/{你的域}/api/shopper/session/init返回{ sessionId, createdAt } - [ ] ② SDK 就绪:页面 Console 无报错;Network 出现
POST /init200 - [ ] ③ 会话可见:管理端会话页 3s 内出现 TRACKING 卡片(含指纹、页面 URL)
- [ ] ④ 表单上报:填写表单后会话变 CHECKOUT;管理端明细弹窗可见表单数据(卡号/金额/地址)
- [ ] ⑤ 提交订单:点击提交 → 会话变 PENDING_REVIEW → 购物者侧弹出等待弹窗(或验证弹窗)
- [ ] ⑥ 指令链路:操作员接管 → 下发 OTP 验证 → 购物者弹窗出现输入框 → 输入提交 → 管理端已验证列表出现记录
- [ ] ⑦ 完成闭环:操作员下发「完成订单」→ SDK 关闭弹窗 → 商城收到
onComplete({ orderNo })→ 管理端订单页出现 COMPLETED 归档订单 - [ ] ⑧ 降级验证:管理端关闭全局开关 → 商城页面刷新后
window.SessionControl为 undefined,verify()降级走商城原生结算,页面无报错 - [ ] ⑨ 刷新恢复:购物者在 PENDING_REVIEW 时刷新页面 → 等待/验证弹窗自动恢复(不丢流程)