Skip to content

SDK API 参考 ​

本文是 Session Control 客户端 SDK 的完整 API 手册。目标:你只读这一篇(配合快速开始的环境准备),就能把 SDK 100% 接入成功。

阅读顺序建议:

  1. 接入前置条件 —— 不满足就开工是接入失败的第一大原因
  2. 加载方式与全局对象
  3. 会话生命周期 —— 理解状态机,后面每个方法都围绕它转
  4. 方法总览 → 逐个方法详解
  5. 回调与命令 payload(自绘 UI 必读)
  6. 错误处理(排错必读)
  7. 接入验收清单(接完照着打勾)

接入前置条件 ​

以下条件缺一不可,任何一个不满足都会导致接入"看起来没生效":

#前置条件不满足时的现象
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 标签(免构建,平台分发,推荐) ​

html
<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(模块化构建) ​

typescript
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 进入就绪态:

  1. 生成/复用浏览器指纹:优先读 localStorage 缓存的指纹(跨刷新/标签页稳定);无缓存时用 FingerprintJS 计算(内部预热两次,取稳定值);FingerprintJS 不可用(隐私模式/加载失败)时降级为随机 ID(也会缓存,避免每次刷新都生成新指纹导致多会话)
  2. 建立会话:POST /api/shopper/session/init(body 携带 siteKey + fingerprint + userAgent + 当前 URL + pageName)。服务端按指纹命中/创建会话并续期
  3. 自动上报当前页面 + 安装 SPA 路由监听(pushState/replaceState/popstate/hashchange)+ 恢复上次会话状态(若上次是 PENDING_REVIEW,重新拉起等待/验证弹窗并启动轮询——刷新页面不丢验证流程)
typescript
init(config: InitConfig): Promise<void>

参数:InitConfig ​

typescript
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
}
参数类型必填默认说明
siteKeystring✅—Site 级 API Key(sk_ 开头,管理端创建 Site 时复制)
pageNamestring否—当前页面名称(如"结算页"),随 init 上报,展示在管理端会话卡片
merchantNamestring否—商户名(如 "Demo Mall"),验证弹窗交易详情条展示("Merchant: xxx")
orderProvider() => Promise<{orderNo, cartItems?} | string>否—订单号提供者。首次 verify() 前 SDK 自动调用它建单。订单号必须由租户服务端真实生成(SDK 不自行生成)。详见下文
formProvider() => FormDataPayload | Promise<FormDataPayload>否—表单兜底:全程未上报过表单时,verify() 主动调它拉一次并上报。返回结构同 reportFormData 参数
formDebounceMsnumber否500reportFormData 内置防抖毫秒数
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 会收到回调

⚠️ 边界与陷阱 ​

  1. 必须 await:init 是 async,未完成时调用其他方法会被静默忽略(页面/表单上报不发送),verify() 返回 'not_ready'。页面加载完成后立即 await init() 再挂表单监听
  2. 可重复调用:init() 可安全调用多次(幂等);destroy() 之后也可重新 init()(复用同一指纹,不重新生成)
  3. SSR 安全:SDK 在非浏览器环境(服务端渲染)不会崩溃,但不会有任何效果——务必在客户端生命周期钩子中调用(Vue: onMounted,React: useEffect)
  4. pageName/merchantName 只在 init 时设置,之后再次调用 init 会整体替换配置
typescript
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 内跳转
  • 其他非标准路由库
typescript
reportPageView(url: string, pageName?: string): void
参数类型必填说明
urlstring✅当前页面完整 URL(window.location.href)
pageNamestring否页面名称(如 "商品页"),展示在管理端会话卡片

行为细节:

  • 同步方法,发出 POST /api/shopper/session/page(异步发送,不阻塞调用方)
  • 调用后更新 SDK 内部“最近上报 URL”记录,后续自动监听路径(pushState/popstate/hashchange 触发)发现 URL 与上次相同则跳过——防止 pushState 与随后的 popstate 重复上报
  • SDK 未就绪 / 已销毁时调用被静默忽略;上报失败静默(不中断用户流程)
typescript
// 例如在 Vue Router 的 afterEach 钩子中
router.afterEach((to) => {
  SessionControl.reportPageView(window.location.href, to.name as string)
})

reportFormData(data) ​

作用:上报结算表单数据。服务端按指纹关联到同一会话,管理端据此核对支付凭据(卡号/金额/地址等),并作为 verify() 的表单完整性校验输入。

核心设计:内置防抖(formDebounceMs,默认 500ms)。表单 input/change 事件里直接调用即可:

  • 本地状态(弹窗卡号/金额展示)实时更新
  • 网络请求合并为"最后一次变更后"上报(连续输入只发一次)
  • verify() 前强制 flush(清掉防抖定时器、立即上报),保证最新表单先到服务端再进验证流程
typescript
reportFormData(data: FormDataPayload): void

参数:FormDataPayload ​

typescript
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 处理
cardNumberstringcredit_card 必填卡号。不能全为空格,否则 verify() 返回 400
cardHolderstring否持卡人姓名(管理端核对用)
expiryDatestring否有效期,如 12/25
cvvstring否CVV。⚠️ 服务端存储用于管理端核验;pending 轮询回读时服务端会剔除卡号/CVV,不会下发到商城链路
billingAddressAddress否账单地址
paypalEmailstringpaypal 必填PayPal 邮箱,不能为空,否则 verify() 返回 400
paypalAccountstring否PayPal 账号
shippingAddressAddress否收货地址
amount / total / orderTotalstring否订单金额(三选一,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):

typescript
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 邮箱/账号):

typescript
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)兜底展示在验证弹窗。

typescript
reportCart(orderNo: string, items: CartItem[]): void
参数类型必填说明
orderNostring✅商城侧订单号(与商城后端一致)
itemsCartItem[]✅购物车商品列表(price × quantity 合计作为金额兜底)
typescript
interface CartItem {
  name: string
  price: number
  quantity?: number
}

什么时候需要调用? ​

一般不需要手动调用:

  • init 配了 orderProvider → SDK 在首次 verify() 前自动建单并调用本方法上报,之后同一会话不再重发(服务端做 CAS 差量合并,不会丢)
  • 不配 orderProvider → 才由接入方在生成订单号后自行调用(此时请在点击提交订单、调 verify() 之前完成)

注意事项:

  • items 决定管理端购物车弹窗内容:只传 orderNo 不传 items(或 cartItems 为空数组)时弹窗出现「订单号有、商品为空」
  • 同一会话内重复调用会覆盖订单号/购物车(幂等场景:重复调用无副作用)
  • SDK 未就绪 / 已销毁时调用被静默忽略
typescript
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 }
typescript
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' 分支做降级放行即可。

typescript
// 用户点击「提交订单」时调用
const { action } = await SessionControl.verify()

switch (action) {
  case 'loading':
    // SDK 已弹出等待/验证弹窗,无需额外处理
    break
  case 'rejected':
    // 拒卡未换卡:顶部通知栏已展示,可引导用户换卡
    break
  case 'not_ready':
  case 'error':
    // 未就绪或失败:提示用户稍后重试(或降级走商城原生结算)
    break
}

边界与陷阱 ​

  1. 连点安全:用户连点提交订单,多个 verify() 共用同一趟建单请求——租户服务端只建一单、/order 只上报一次(否则旧单号会成为租户侧孤儿订单)。完成订单后 SDK 重建会话,闸门同步重置,新一轮结算重新建单
  2. complete_order 不通过 onCommand 回调下发:服务端下发即归档删键,SDK 在轮询发现会话离开 PENDING_REVIEW 时回调 onComplete(状态兜底)
  3. onComplete 有 15s 新鲜度门槛:仅当弹窗开着、且距上次成功轮询不超过 15s 才判定为"订单完成"。页面被挂起超过 15s(典型:App 验证跳去银行 App 再回来)时,会话可能已 TTL 过期重建,此时不会回调 onComplete——接入方不要把完成页跳转只挂在这个回调上(建议同时监听自己的订单状态接口)
  4. 管理端未接管也可下发指令;接管只是绑定操作员展示,不影响命令下发
  5. TRACKING 状态下管理端无法下发指令(isCommandIssuable 限制),所以必须先上报表单再 verify,否则会话停在 TRACKING 时管理端没法操作

submitVerification(commandId, value) ​

作用:提交验证码/整值到服务端。SDK 内置弹窗会在用户点击确认时自动调用;此方法供商城自绘验证 UI(配合 onCommand 回调)时使用。

typescript
submitVerification(commandId: string, value: string): Promise<boolean>
参数类型必填说明
commandIdstring✅onCommand 回调中 command.commandId(服务端下发时生成,必须原样回传)
valuestring✅验证码/整值(长度上限 4096)

返回值 ​

Promise<boolean>:是否上报成功。

  • true:服务端已记录(命令置为 completed,管理端已验证列表可见)
  • false:失败(网络/服务端错误,或命令已失效)。内置弹窗会自动恢复输入框与确认按钮允许重试;自绘 UI 时请根据返回值恢复输入,否则用户会卡死

服务端只接受 status === 'active' 的命令:无效 commandId 或已完成的命令一律 400(防随意提交污染验证记录)。若操作员已把该命令作废/换新命令,提交会失败。

typescript
// 自绘验证 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。

typescript
destroy(): void

行为细节:

  • 同步方法,立即生效
  • 销毁后可重新 init():复用 localStorage 缓存的同一指纹重建/续期会话(不重新生成指纹)
  • 所有内部定时器(轮询、防抖)与事件监听全部清理,不产生泄漏

⚠️ 边界与陷阱 ​

  • destroy() 不重置已缓存的订单号与"已上报过表单"标记(只有 complete_order 后的内部重建会重置):同一页面实例 destroy → init 后,verify() 不会再次调 orderProvider 建单
  • 若原会话已 TTL 过期,服务端新会话没有表单,verify() 会返回 400(需先重新上报表单)
  • 通常不需要调用:对单页应用,最规范的做法是离开结算页(组件卸载)时调用,返回时重新 init()
typescript
// SPA 离开结算流程时
onUnmounted(() => {
  SessionControl.destroy()
})

回调与命令 payload ​

onCommand 回调与命令 payload ​

管理端每下发一条命令,SDK 在渲染弹窗的同时调用 onCommand(cmd)。内置弹窗已自动处理全部 8 种命令,只有自绘 UI / 埋点才需要关心 payload。命令类型与 payload 字段:

命令类型 type含义payload 关键字段弹窗行为
otp_verificationOTP 短信验证码phone?(尾号展示用,如 {phone: '138****8000'} 取后4位)输入框(数字键盘)
phone_verification手机号验证码phone?输入框(数字键盘)
email_verification邮箱验证码email?(打码展示,如 q****ctb@iubridge.com)输入框
app_verificationApp 推送验证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:

typescript
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 ​

typescript
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
}
字段类型说明
commandIdstring命令唯一 ID(cmd_ 开头),submitVerification 需原样回传
typestring命令类型(8 种,见上文 payload 表)
payloadobject命令参数(提示文案、手机/邮箱、银行名、金额等,按 type 变化)
status'active' | 'completed' | 'interrupted'命令状态:下发为 active;验证提交后 completed;会话中断/重建后作废
operatorId? / operatorName?number / string下发操作员
issuedAtnumber下发时间戳(ms)

payload 里字段的具体含义见命令 payload 表。拒卡命令绑定了下发时的卡号:购物者换卡后旧拒卡命令不再返回(服务端按卡号过滤),所以 onCommand 收到 reject_card 时针对的一定是当前卡。

SDKError ​

typescript
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 KeysiteKey 错误/站点已删除/站点已停用核对管理端 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 标签) ​

html
<!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 包) ​

vue
<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?)
表单填写完成/changeSessionControl.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 /init 200
  • [ ] ③ 会话可见:管理端会话页 3s 内出现 TRACKING 卡片(含指纹、页面 URL)
  • [ ] ④ 表单上报:填写表单后会话变 CHECKOUT;管理端明细弹窗可见表单数据(卡号/金额/地址)
  • [ ] ⑤ 提交订单:点击提交 → 会话变 PENDING_REVIEW → 购物者侧弹出等待弹窗(或验证弹窗)
  • [ ] ⑥ 指令链路:操作员接管 → 下发 OTP 验证 → 购物者弹窗出现输入框 → 输入提交 → 管理端已验证列表出现记录
  • [ ] ⑦ 完成闭环:操作员下发「完成订单」→ SDK 关闭弹窗 → 商城收到 onComplete({ orderNo }) → 管理端订单页出现 COMPLETED 归档订单
  • [ ] ⑧ 降级验证:管理端关闭全局开关 → 商城页面刷新后 window.SessionControl 为 undefined,verify() 降级走商城原生结算,页面无报错
  • [ ] ⑨ 刷新恢复:购物者在 PENDING_REVIEW 时刷新页面 → 等待/验证弹窗自动恢复(不丢流程)