核对日期:2026-10-07。以下按当前服务端、Web 与 App 宿主实现说明;环境地址和应用配置以平台管理员提供的值为准。
第三方应用不接收 IM 登录 token。client_secret 只保存在自己的后端(BFF),不放进前端代码或 URL。授权码、跳转票及 OAuth token 不写入日志;跳转票不放进 query / hash。
接入总览:一次登录,衔接钱包互转
本文统一说明工作台网页应用的 OAuth 登录与简聊钱包支付通道。只接登录,阅读第 1–8 节;还需钱包互转,使用同一个 confidential 应用,继续阅读第 9–14 节。
- 配置应用:取得 issuer、host_web_origin、client_id;需要钱包时还需 client_secret、通道币种 / 汇率和独立的 webhook_secret。
- 完成登录:通过授权码或端内跳转票,在自己的 BFF 建立会话,保存可信的
(issuer, sub)。 - 创建订单:BFF 从当前会话读取 sub,生成并持久化 merchant_order_no,再调用充值或提现接口。不能接受浏览器任意指定收款用户。
- 确认入账:充值通过端内宿主支付单扣款;BFF 验证回调或主动查单,再按本地订单幂等更新应用账本。
| 操作(以应用为视角) | 余额流向 | 对应接口 |
|---|---|---|
| 充值到应用 | 用户简聊钱包 → 应用余额 | prepay → 宿主支付单 → 回调 / 查单入账 |
| 提现到简聊 | 应用余额 → 用户简聊钱包 | 应用先扣减自身余额 → payout → 确认结果 |
这里的互转是配置币种的虚拟余额通道。钱包商户接口使用与 OAuth 相同的 client_id / client_secret 认证;用户 access_token 不能代替商户认证。webhook_secret 专门用于回调验签,与 client_secret 分开管理。
1. OAuth:准备应用配置
- 在简聊工作台登记网页应用,网页地址必须是与简聊 Web / API 等平台地址不同源的绝对 HTTPS 地址,不能带 fragment。
- 启用 OAuth,选择客户端类型、登记回调地址及允许的 scope;把应用加入工作台分组或轮播。
- 向管理员获取下面的配置。confidential 应用还需生成密钥;轮换后应立即更新 BFF。
| 配置项 | 示例 | 用途 |
|---|---|---|
issuer | https://api.example.com/v1 | API 的 /v1 根,无末尾斜杠;发现、换 token、取用户信息。 |
host_web_origin | https://im.example.com | 简聊 Web 的 origin(协议 + 域名 + 可选端口),用于验证 iframe 父窗口。不能用 issuer 或 API origin 代替。 |
client_id | your-app-id | 工作台应用 APP ID。 |
client_type | public / confidential | public 无密钥;confidential 有可信后端和 client_secret。 |
redirect_uri | https://app.example.com/oauth/callback | 必须预登记,授权申请和兑换时完全一致(含路径、查询参数、末尾斜杠)。不支持通配符、fragment。 |
scope | openid profile | 必含 openid;仅请求已开通且业务需要的权限。 |
本文 Demo 的网页地址为 https://app.example.com/,回调为 https://app.example.com/oauth/callback。文中所有 example.com 地址都需要替换。
2. 先选择登录流程
| 打开环境 | public | confidential |
|---|---|---|
| 简聊 Web iframe | hello → ready → request(PKCE)→ code → token → userinfo | hello → ready → jump → BFF redeem → 本地会话 |
| 简聊 App WebView | 原生 auth JSBridge 携带 PKCE → 授权码 → token → userinfo | hello → ready → jump → BFF redeem → 本地会话 |
| 外部 / 系统浏览器 | 先建立 IdP 浏览器会话 → authorize(PKCE)→ 回调 code + state → token → userinfo | |
所有授权码流程都必须使用 PKCE S256,包括 confidential。端内 confidential 的跳转票是独立流程:不需要主动发送 request,不是“先收 code,再收 jump”,redeem 也不返回 OAuth token。
Web iframe 的 public 示例和外部浏览器示例均包含在下方 Demo。App confidential 支持点击握手;App public 的原生 JSBridge 适配不在此 Demo 范围内。
3. 发现与公开接口
GET {issuer}/.well-known/openid-configuration
发现文档提供 issuer、各端点、RS256 验签算法、S256 PKCE 和支持的 scopes。对接固定可信环境,检查返回的 issuer 与配置一致。
| 用途 | 方法和路径 | 说明 |
|---|---|---|
| 授权页 | GET {issuer}/oauth/authorize | 浏览器顶层导航;依赖 IdP 会话。 |
| 兑换 / 刷新 | POST {issuer}/oauth/token | 表单或 JSON;confidential 支持 HTTP Basic 或 client_secret 字段。 |
| 用户信息 | GET {issuer}/oauth/userinfo | Authorization: Bearer access_token。 |
| 撤销 token | POST {issuer}/oauth/revoke | 表单或 JSON 的 token 字段。 |
| 验签公钥 | GET {issuer}/oauth/jwks | 验证 id_token 使用。 |
| 兑换跳转票 | POST {issuer}/oauth/jump/redeem | 仅 confidential BFF;JSON,无 CORS。 |
4. 端内:握手、授权码与跳转票
Web iframe 的握手
先注册 message 监听,再发 hello。除不含凭据的 hello 外,postMessage 必须指定准确 origin。接收时同时检查 event.source === window.parent 和预配置的 host_web_origin;不能把任意 ready 消息中的 parent_origin 当成可信来源。
// config 来自自己后端的可信配置;不要从 URL 参数覆盖。
window.addEventListener("message", (event) => {
if (event.source !== window.parent ||
event.origin !== config.hostWebOrigin) return;
const data = event.data;
if (!data || typeof data !== "object") return;
if (data.type === "tsdd.oauth.ready") {
if (data.parent_origin !== config.hostWebOrigin ||
data.app_id !== config.clientId) return;
// public:只发送一次带 PKCE 的 request。
// confidential:等待 jump,不再发送 request。
}
});
window.parent.postMessage({ type: "tsdd.oauth.hello" }, "*");
public:申请并接收授权码
每次登录生成随机 state(至少 16 字符)和 code_verifier(43–128 位允许字符),challenge 为 SHA-256(verifier) 的无 padding base64url 编码。Demo 在 BFF 生成并保存 verifier,浏览器只拿到 challenge。
window.parent.postMessage({
type: "tsdd.oauth.request",
client_id: config.clientId,
redirect_uri: "https://app.example.com/oauth/callback",
scope: "openid profile",
state,
code_challenge,
code_challenge_method: "S256"
}, config.hostWebOrigin);
// 宿主响应(示意):
{ "type": "tsdd.oauth.code", "code": "oac_...",
"state": "原样返回的state", "expires_in": 60 }
宿主固定当前工作台应用身份。收到 code 后,必须检查来源及 state 与本次事务完全一致;缺少 state、没有待处理事务、重复结果都不能继续登录。用同一事务的 verifier 兑换 code。处理 tsdd.oauth.denied、tsdd.oauth.error,不要自动无限重试。
confidential:接收跳转票并交给 BFF
// 宿主在 ready 后下发(授权弹窗由宿主处理):
{ "type": "tsdd.oauth.jump", "app_id": "your-app-id",
"token": "ajw_1....", "expires_in": 60 }
// 校验消息来源、app_id,且本次登录尚未处理结果后:
await fetch("/api/jump", {
method: "POST",
credentials: "same-origin",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token: data.token, csrf: transaction.csrf })
});
/api/jump 是下方 Demo 自己的 BFF 路由。BFF 使用配置好的 client_id 与当前密钥调用:
POST /v1/oauth/jump/redeem
Content-Type: application/json
{"token":"ajw_1....","client_id":"your-app-id","client_secret":"仅后端持有"}
// 成功响应示意(name / picture 按授权返回):
{"sub":"user-uid","app_id":"your-app-id","exp":1791331260,
"name":"示例用户","picture":"https://api.example.com/users/user-uid/avatar"}
- 跳转票约 60 秒有效、一次性。轮换密钥后未兑票失效。兑换失败应重新发起登录,不重复使用旧票。
- 成功响应是身份信息,没有 access_token、id_token 或 refresh_token。exp 是跳转票到期时间,本地会话有效期由业务系统管理。
- 当前跳转票申请权限为 openid,以及应用允许时的 profile;需要 email / phone 时使用相应授权码流程。
- 登录只认在线 redeem;不需要本地 AES 解密,解密结果也不能代替兑换校验。
- BFF 验证返回 app_id、sub 等字段后,以
(issuer, sub)关联自己的用户,生成 HttpOnly 会话 Cookie。
App WebView 的区别
当前 Android / iOS confidential 宿主向页面自身 window 注入 ready / jump,origin 为应用页面 origin;不是 Web iframe 的父窗口 origin。应明确选择 App 适配路径并校验 event.source === window、event.origin === location.origin 和 app_id。Demo 的「端内登录」按钮在顶层页面使用此路径。public 则需要客户端原生 auth JSBridge,不能只发送 Web iframe 的 request。
5. 外部浏览器:授权码 + PKCE
直接打开 authorize 不会展示账号密码登录页。当前实现依赖简聊的 IdP 浏览器会话。应由用户从已登录的简聊工作台外开应用,宿主先通过 {issuer}/oauth/session 建立会话,再跳到登记的网页地址。第三方无需自己获取宿主票据或持有 IM token。
应用生成并保存本次 state、verifier,然后顶层跳转:
GET {issuer}/oauth/authorize?response_type=code
&client_id=your-app-id
&redirect_uri=https%3A%2F%2Fapp.example.com%2Foauth%2Fcallback
&scope=openid%20profile
&state=随机且至少16字符
&code_challenge=BASE64URL_SHA256_VERIFIER
&code_challenge_method=S256
用 URL / URLSearchParams 构造一条完整 URL,不要直接拼接未编码的参数。成功回调携带 code、state;拒绝或需要授权时可能携带 error、state。成功和错误回调都先验证 state;兑换后清理回调 URL 与待处理事务。
IdP 会话当前为 600 秒;失效会得到 oauth_login_required。返回简聊重新从工作台打开。prompt=none 不能绕过同意,可能返回 consent_required。
6. Token、用户字段和有效期
POST {issuer}/oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=authorization_code&client_id=your-app-id&code=oac_...
&redirect_uri=https%3A%2F%2Fapp.example.com%2Foauth%2Fcallback
&code_verifier=本次保存的verifier
// confidential 还需由后端添加 client_secret,或使用 HTTP Basic。
// 响应示意:
{"access_token":"oat_...","token_type":"Bearer","expires_in":3600,
"scope":"openid profile","id_token":"eyJ...","refresh_token":"ort_..."}
public 响应不包含 refresh_token。授权码 60 秒有效且只能使用一次,access_token 当前为 3600 秒,confidential 的 refresh_token 当前为 14 天;以响应和服务端实际校验为准。
GET {issuer}/oauth/userinfo
Authorization: Bearer oat_...
{"sub":"user-uid","name":"示例用户",
"picture":"https://api.example.com/users/user-uid/avatar"}
| scope | userinfo 字段 |
|---|---|
| openid | sub:用户标识,始终返回。 |
| profile | name、picture:昵称和头像。 |
| email:授权且账号有邮箱时返回。 | |
| phone | phone_number:授权且账号有手机号时返回。 |
权限受后台配置与实际授权限制,不保证请求的 scope 全部获批。userinfo 不接受 query 中的 access_token;public 可从已登记回调地址的 origin 直调 token / userinfo(CORS 不允许 credentials),也可采用本 Demo 的 BFF 方式。confidential 的密钥兑换始终在后端完成。
若用 id_token 建立身份,必须通过 JWKS 验证 RS256 签名及 iss、aud、exp,并校验请求时保存的 nonce(如果使用)。不要只 base64 解码就信任。本 Demo 以 BFF 通过 Bearer 请求的 userinfo 建立身份,不使用 id_token。
// 刷新:仅 confidential 后端,表单提交并携带客户端认证
POST {issuer}/oauth/token
grant_type=refresh_token&refresh_token=ort_...&client_id=...&client_secret=...
// 撤销:表单或 JSON
POST {issuer}/oauth/revoke
token=需要撤销的access_token或refresh_token
刷新会轮换 refresh_token:原子保存新值,避免并发刷新;重放旧 refresh_token 会撤销该刷新令牌族。撤销接口返回 200 不代表 token 原先一定有效。本地退出还需清理自己的会话。
7. OAuth Demo:前端 + Node.js
两个文件放在同一目录,Node.js 22+,无需 npm 依赖:
- 下载 server.mjs:生成 PKCE / state、验证回调、兑换 code / jump、请求 userinfo、建立本地会话。
- 下载 index.html:Web iframe / App confidential 消息接收、发起浏览器授权、显示当前用户。
把应用 HTTPS 域名反向代理到本机 127.0.0.1:3000(保留路径),在管理后台登记上面第 1 节的网页地址与回调地址,然后启动:
export OAUTH_ISSUER='https://api.example.com/v1'
export HOST_WEB_ORIGIN='https://im.example.com'
export APP_ORIGIN='https://app.example.com'
export OAUTH_CLIENT_ID='your-app-id'
# confidential:通过服务端环境或密钥管理器注入 OAUTH_CLIENT_SECRET。
# public:不设置 OAUTH_CLIENT_SECRET;不要将其写进 HTML 或提交到仓库。
node server.mjs
- 从简聊工作台打开 Demo:Web iframe 自动握手。public 会发送 PKCE 请求;confidential 等待 jump 并 POST 给 BFF。
- App confidential 在页面中点「端内登录」。App public 需要另接原生 JSBridge。
- 系统浏览器模式从工作台外开后,点「浏览器登录」;BFF 保存 verifier 并返回授权 URL,回调验证 state 后建立会话。
- 完成登录后点「读取本地会话」,应看到 issuer、sub 及授权后的昵称头像;浏览器不会收到密钥或 OAuth token。
BFF 的 /api/start、/api/code、/api/jump、/api/me 是示例自身的接口,不是平台 OpenAPI。state、verifier 和 token 均由 BFF 管理。POST 校验 Origin,结果提交还校验与会话绑定的 csrf;消息去重后只兑换一次。
Demo 仅演示登录:会话保存在单进程内存,重启即丢失;不持久化 OAuth token,也不演示刷新。上线时接入业务用户库、共享会话存储、限流与退出逻辑。Cookie 使用 Secure / HttpOnly / SameSite=None,必须通过 HTTPS 访问。跨站 iframe 仍可能被浏览器第三方 Cookie 策略拦截,遇到此情况请通过工作台外开浏览器完成登录,不能靠反复兑换票据解决。
关键后端兑换代码(完整错误处理见下载文件)
// BFF 使用预配置的 issuer、clientId、clientSecret。
const response = await fetch(`${issuer}/oauth/jump/redeem`, {
method: "POST",
redirect: "error",
signal: AbortSignal.timeout(10000),
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ token, client_id: clientId, client_secret: clientSecret })
});
const user = await response.json();
if (!response.ok) throw new Error(user.code || "redeem failed");
if (user.app_id !== clientId || !user.sub || user.exp * 1000 <= Date.now()) {
throw new Error("invalid identity");
}
// 用 (issuer, user.sub) 查找/创建本地用户,轮换会话 ID 并设置 HttpOnly Cookie。
// 不把 token、client_secret 或完整上游响应写入日志。
8. OAuth 常见问题
| 现象 | 检查项 / 处理 |
|---|---|
| 网页地址保存失败 | 绝对 HTTPS、与平台不同源、无 fragment;确保应用是网页类型且已上架。 |
| iframe 等不到 ready | 先监听再 hello;host_web_origin 填简聊 Web origin,不是 API 域名;确认窗口来源与 app_id。 |
| App 没有 code | public 需要原生 auth JSBridge;confidential 等待 jump;旧客户端需升级。 |
| oauth_login_required | IdP Cookie 未建立或已过期。从已登录的简聊工作台重新外开应用。 |
| invalid_request / oauth_invalid_redirect / oauth_pkce_required | 回调地址精确匹配、scope 含 openid、state 至少 16 字符、S256 challenge 正确。 |
| invalid_client | 检查 client_id、类型和当前 client_secret,确认应用 OAuth 已启用。 |
| invalid_grant | code 已使用或过期、verifier 不匹配、refresh_token 已失效;重新发起登录,不重试旧凭据。 |
| jump 兑换失败 | 票超过 60 秒、重复兑换、密钥已轮换、app_id 不匹配、用户授权已撤销等。重新打开应用获取新票。 |
| consent_required / access_denied | 需要用户同意或用户拒绝;交还用户操作,不静默重试。 |
| Demo 登录事务失效 / 登录后仍 401 | 确认 HTTPS 代理、浏览器 Cookie 策略及单实例会话;多实例需共享存储。 |
token 接口错误通常为 {error, error_description};userinfo / jump redeem 错误为 {status, msg, code}。authorize 还可能返回 HTML 错误页或携带 error、state 的回调。先检查 HTTP 状态,再按接口解析;日志仅保留错误码等非凭据字段。
9. 钱包互转:开通条件与共用身份
简聊钱包支付通道的商户接口无 CORS,只能由自己的 BFF 调用。client_secret 与 webhook_secret 留在后端;支付密码只在宿主支付单输入,应用页面只传 prepay_id。
仅已登记的工作台网页应用,且为 confidential(有 client_secret)、
已按前文登录流程将当前用户 sub 保存到自己的服务端会话。
public 客户端不能开通此通道;系统浏览器没有端内支付单,页面不展示「简聊钱包」渠道。
请管理员启用钱包模块、应用 OAuth 和支付通道,配置 IM / 应用币种、汇率、HTTPS 回调地址并生成 webhook_secret。充值成功增加通道头寸,提现成功减少头寸;提现前需确认头寸足够。
- 认证:HTTP Basic
client_id:client_secret,POST 也可用 JSON 字段client_id+client_secret;GET 查单使用 HTTP Basic。 - 金额:正整数 int64 最小单位,禁止 float;JavaScript 应限制在安全整数范围,或使用能精确处理 int64 的序列化方案。
- 汇率由简聊管理端配置;
im_amount = app_amount * rate_im / rate_app必须整除,否则wallet_channel_rate。 - 幂等:同一
app_id + merchant_order_no重复 prepay / payout 返回原单,不双扣。充值和提现使用不同单号;单号最多 64 字节。同一笔业务重试必须复用单号,并核对返回订单的用户、方向、金额。
10. 钱包商户接口:充值、提现、查单
issuer 是 API 的 /v1 根。前缀 {issuer}。JSON snake_case。错误体 {status,msg,code},客户端不要解析中文 msg。
| 方法 | 路径 | 说明 |
|---|---|---|
| POST | {issuer}/wallet/channel/prepay | 充值预付单 |
| POST | {issuer}/wallet/channel/payout | 提现入用户简聊钱包 |
| GET | {issuer}/wallet/channel/merchant/orders?merchant_order_no= | 查单(必填商户单号) |
下面的 sub 必须来自 BFF 当前登录会话;不要从 query 或前端表单直接取值。先持久化本地订单,再请求平台,避免响应丢失后无法定位订单。
prepay 请求:
POST {issuer}/wallet/channel/prepay
Content-Type: application/json
Authorization: Basic base64(client_id:client_secret)
{
"merchant_order_no": "c_recharge_…",
"sub": "<uid>",
"app_amount": 10000,
"notify_url": "https://app.example/callbacks/jianliao-wallet"
}
notify_url 若传必须是 https 绝对 URL;不传则用通道配置的 webhook。
两者都空 → wallet_channel_notify_required。
成功(prepay_id 与 order_no 均为 wcp_ 前缀):
{
"prepay_id": "wcp_…",
"order_no": "wcp_…",
"im_currency": "COIN",
"im_amount": 10000,
"app_currency": "PLATFORM_COIN",
"app_amount": 10000,
"rate_im": 1,
"rate_app": 1,
"expire_at": "2026-08-22T01:00:00Z",
"status": "prepay"
}
payout 请求字段与 prepay 相同;响应无 prepay_id 和 expire_at。成功 status=paid,order_no 前缀 wco_。
提现前你必须先扣自己的余额;简聊头寸不足返回 wallet_channel_float_insufficient 且不加款,你必须把已扣余额退回用户。
提现不弹简聊支付密码。
POST {issuer}/wallet/channel/payout
Content-Type: application/json
Authorization: Basic base64(client_id:client_secret)
{"merchant_order_no":"app_payout_001","sub":"来自当前会话的uid","app_amount":10000}
GET {issuer}/wallet/channel/merchant/orders?merchant_order_no=app_payout_001
Authorization: Basic base64(client_id:client_secret)
网络超时或连接中断时结果未知:保留原商户单号查单 / 重试确认,不要直接退回余额后用新单号再次提现。只有确认未成功出款,才能执行本地补偿。
密钥认证失败返回 401 oauth_invalid_client,不泄露订单是否存在;认证正确但单号不存在时返回 wallet_not_found。
11. 端内唤起支付单
与 OAuth 相同:先 listen,再 tsdd.oauth.hello(仅这一次允许 *)。
按OAuth 章节验证来源、app_id 和 ready 后复用同一个宿主连接,无需再次兑换跳转票。
禁止 postMessage(..., "*") 发送 tsdd.wallet.pay。
系统浏览器忽略该消息,不要展示「简聊钱包」渠道。
Web iframe 的目标是 window.parent,origin 为已验证的 host_web_origin;App WebView 的目标是 window,origin 为当前应用 origin。以下为 iframe 示例:
window.parent.postMessage(
{ type: "tsdd.wallet.pay", prepay_id: "wcp_…" },
parent_origin
);
接收结果时验证 event.source、event.origin 和当前订单的 prepay_id,再更新页面状态:
{
"type": "tsdd.wallet.pay.result",
"prepay_id": "wcp_…",
"status": "paid|cancelled|failed",
"code": "wallet_insufficient?"
}
cancelled:用户关掉支付单,预付单仍有效直至 TTL(10 分钟)。failed:带稳定code。余额不足为wallet_insufficient,关支付单并回此结果;订单仍为prepay,不扣款、不发 webhook。密码错留在支付单上,不发 result。paid:简聊已扣用户款;你仍须等自己的 webhook / 查单后再给应用余额入账。
页面只轮询自己的商户订单。不要在应用页收集支付密码。
12. 回调 HMAC 与对账
POST notify_url,超时 5s。返回 HTTP 2xx 视为成功。验签失败不得入账。按 app_id + merchant_order_no 幂等;比较 app_id、sub、direction、金额和币种与本地订单一致后,在同一事务中记录入账并更新订单状态。
使用收到的原始请求字节 raw_body 计算签名,不能 JSON.parse 后重新序列化。使用恒定时间比较,并检查时间戳窗口(参考 Demo 为前后 5 分钟)。webhook 与主动查单补账必须共用同一套幂等入账逻辑。
Headers:
Content-Type: application/jsonX-Tsdd-Channel-Timestamp:unix 秒X-Tsdd-Channel-Signature:sha256=+ hex(HMAC-SHA256(webhook_secret,timestamp + "." + raw_body))
{
"order_no": "wcp_…",
"merchant_order_no": "…",
"app_id": "…",
"sub": "…",
"direction": "pay",
"status": "paid",
"im_currency": "COIN",
"im_amount": 10000,
"app_currency": "PLATFORM_COIN",
"app_amount": 10000,
"paid_at": "…"
}
投递失败会按 1s / 5s / 30s / 2m / 10m 最多重试 5 次,耗尽后不再投。
你必须能 GET 查单补账。固定向量(仅用于自测,不是生产密钥):
secret wcs_test_secret,timestamp 1700000000,
body {"order_no":"wcp_ab","status":"paid"} →
sha256=f9481542f257bb171be3370459fcf0b7839136c3a6aaf5219228b74952e8d831。
13. 钱包常见错误码
| code | 何时 |
|---|---|
oauth_invalid_client | 密钥错 |
wallet_channel_disabled | 未开通道或非 confidential |
wallet_channel_rate | 金额与汇率无法整除 |
wallet_channel_notify_required | 无 webhook 且无 notify_url |
wallet_channel_float_insufficient | 提现头寸不足 |
wallet_not_found | 查单不存在(密钥正确时) |
wallet_disabled | 钱包模块关闭 |
wallet_rate_limited | HTTP 429,请求过于频繁,稍后再试。 |
探测:非法单查单,期望 wallet_not_found 或错密钥 oauth_invalid_client。
14. 钱包对接 Demo
OAuth 登录可先运行第 7 节的 Node.js Demo。钱包交互另提供 Go BFF + 页面参考文件,集中在这里下载:
- 下载 wallet-channel-sample.html:端内握手、跳转票提交、唤起支付和订单状态展示。
- 下载 wallet-channel-sample-bff.go:redeem、prepay、payout、查单与 HMAC 验签。
把两个文件放到同一目录,准备跨源 HTTPS 应用域名与能接收平台回调的地址,代理到 127.0.0.1:8787,然后配置:
export TSDD_API='https://api.example.com/v1' export PUBLIC_BASE='https://wallet-app.example.com' export CLIENT_ID='your-app-id' # 从服务端环境注入 CLIENT_SECRET 与 WEBHOOK_SECRET。 # 登记此应用地址,并配置通知地址: # https://wallet-app.example.com/callbacks/jianliao-wallet go run wallet-channel-sample-bff.go
两个 Demo 独立运行,不共享内存会话。正式接入时,把钱包接口接到同一个业务 BFF,从已经建立的登录会话读取 sub,并复用已验证的宿主消息来源;不要把一次性跳转票分别交给两个后端重复兑换。
钱包样例用于阅读和测试接口流程:余额是单进程共享的模拟值,未实现按用户持久化账本;其会话 Cookie、消息来源校验和提现失败补偿也需按本文完善。尤其不能照搬“网络错误立即退款”的简化逻辑。真实接入需实现按用户记账、订单事务、查单补偿,以及端内跨站 Cookie 适配。
只在已验证的简聊端内展示「简聊钱包」渠道;普通浏览器不展示。卡通(独立仓库 cartoon)是现有商户示例,汇率以简聊管理端配置为准。