跳到主要内容

注册与授权(绑定)

用户通过 PalmxApp 扫描网页二维码,将 Palm 账号与您系统中的用户账号建立关联。

架构说明

SDK 采用 iframe + postMessage 架构,彻底避免跨域问题:

┌─ 客户网站 (www.customer.com) ─────────────────────────┐
│ │
│ <script src="palm-oauth.js"> │
│ ┌─ iframe (open.palmoa.youtu.qq.com/palm/x/h5/oauth/qr/page) ──┐│
│ │ ││
│ │ [二维码] ← 同域请求,无跨域 ││
│ │ "请使用 PalmxApp 扫码绑定" ││
│ │ ││
│ │ 轮询状态... ││
│ │ 绑定成功 → postMessage({code, state}) ────────── ││──→ SDK 接收
│ └────────────────────────────────────────────────────┘│
│ │
│ SDK 将 { code, codeVerifier, state } 通过 Promise │
│ 返回给调用者,调用者自行发送给自己的后端完成绑定 │
│ │
└────────────────────────────────────────────────────────┘

快速开始

1. 引入 SDK

<script src="https://open.palmoa.youtu.qq.com/sdk/palm-oauth.js"></script>

就这一行,无需引入二维码库(二维码在 iframe 内渲染)。

2. 使用

<div id="bindContainer"></div>

<script>
PalmOAuth.init({
appId: 'your_app_id',
});

PalmOAuth.bind('#bindContainer')
.then(function (result) {
// result = { code, codeVerifier, state }
// 将结果发送给您的后端,完成账号绑定
fetch('/api/bindPalm', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + userToken, // 您系统的登录态
},
body: JSON.stringify(result),
}).then(function () {
alert('绑定成功!');
});
})
.catch(function (err) {
console.error('绑定失败:', err.message);
});
</script>

配置项

PalmOAuth.init({
// 必填
appId: 'your_app_id',

// 可选
baseUrl: 'https://open.palmoa.youtu.qq.com', // 接口域名
width: 300, // iframe 宽度 px
height: 360, // iframe 高度 px
lang: 'zh-CN', // 语言:zh-CN / en

// 事件回调
onStateChange: function (status) {}, // pending / scanned / confirmed / denied / expired
onSuccess: function (result) {}, // result = { code, codeVerifier, state }
onError: function (err) {}, // 错误
});

API

方法说明
PalmOAuth.init(options)初始化配置
PalmOAuth.bind(container)在指定容器内展示二维码,发起绑定流程,返回 Promise
PalmOAuth.cancel()取消当前绑定流程,移除 iframe
PalmOAuth.destroy()销毁实例

后端对接

SDK 绑定成功后,Promise resolve 返回以下数据:

{
code: "f30cb9df-dc28-4a28-858e-0eb7c6f59c67", // 授权码
codeVerifier: "P25ffL26KVLZozkFdAQ2yxRZADK9Y88NDu8WlIYhdA", // PKCE 密钥
state: "state_1700000000_abc123" // 防 CSRF 随机串
}

您的前端拿到后,发送给自己的后端(必须带上您系统的用户登录态,以便后端知道是哪个用户要绑定):

PalmOAuth.bind('#container').then(function (result) {
fetch('/api/bindPalm', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + userToken, // 您系统的登录态(必须)
},
body: JSON.stringify({
code: result.code,
code_verifier: result.codeVerifier,
}),
});
});

您的后端收到后,调用 Palm Token 接口换取 open_id,并与当前登录用户建立绑定关系:

// POST /api/bindPalm
func BindPalmHandler(w http.ResponseWriter, r *http.Request) {
// 1. 验证当前登录用户(从您自己的登录态中获取)
currentUser := GetCurrentUser(r)

// 2. 解析请求体
var req struct {
Code string `json:"code"`
CodeVerifier string `json:"code_verifier"`
}
json.NewDecoder(r.Body).Decode(&req)

// 3. 用 code 换取 Palm open_id
body, _ := json.Marshal(map[string]string{
"GrantType": "authorization_code",
"Code": req.Code,
"CodeVerifier": req.CodeVerifier,
})
httpReq, _ := http.NewRequest("POST",
"https://open.palmoa.youtu.qq.com/palm/openai/x/oauth/token",
bytes.NewReader(body))
httpReq.Header.Set("Content-Type", "application/json")
httpReq.Header.Set("Authorization", "Bearer "+API_KEY)

resp, _ := http.DefaultClient.Do(httpReq)
defer resp.Body.Close()

var result struct {
Data struct {
OpenId string `json:"OpenId"`
AccessToken string `json:"AccessToken"`
RefreshToken string `json:"RefreshToken"`
} `json:"data"`
}
json.NewDecoder(resp.Body).Decode(&result)

// 4. 绑定 & 保存 token
db.BindPalmAccount(currentUser.ID, result.Data.OpenId)
db.SavePalmTokens(currentUser.ID, result.Data.AccessToken, result.Data.RefreshToken)

json.NewEncoder(w).Encode(map[string]interface{}{
"success": true, "message": "绑定成功",
})
}

完整 HTML 示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>绑定 Palm 账号</title>
<style>
body {
display: flex; justify-content: center; align-items: center;
min-height: 100vh; margin: 0;
font-family: -apple-system, sans-serif; background: #f5f7fa;
}
.bind-card {
background: #fff; border-radius: 12px; padding: 32px;
box-shadow: 0 2px 12px rgba(0,0,0,0.08); text-align: center;
}
.bind-card h2 { margin: 0 0 8px; color: #333; }
.bind-card p { margin: 0 0 20px; color: #666; font-size: 14px; }
</style>
</head>
<body>
<div class="bind-card">
<h2>绑定 Palm 账号</h2>
<p>使用 PalmxApp 扫描下方二维码,将您的 Palm 账号与本站账号绑定</p>
<div id="bindContainer"></div>
</div>

<script src="https://open.palmoa.youtu.qq.com/sdk/palm-oauth.js"></script>
<script>
PalmOAuth.init({
appId: 'your_app_id',
onStateChange: function (s) { console.log('状态:', s); },
});

PalmOAuth.bind('#bindContainer')
.then(function (result) {
// 注意:后端期望的字段名是 code_verifier(下划线),而 SDK 返回的是 codeVerifier(驼峰)
fetch('/api/bindPalm', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
code: result.code,
code_verifier: result.codeVerifier,
}),
}).then(function () {
alert('绑定成功!');
window.location.reload();
});
})
.catch(function (e) { alert('绑定失败: ' + e.message); });
</script>
</body>
</html>