微信小程序嵌入 H5 登录对接文档
本文档面向第三方小程序开发团队,说明如何在微信小程序中通过 web-view 嵌入构播云 H5 直播间,并完成微信小程序登录对接。
约定:下文中 小程序端 指接入方微信小程序,平台方 指我方。
1. 方案概述
小程序嵌入 H5 时,H5 本身不处理登录,而是由小程序端完成微信登录后获取平台方 Token,再透传给 H5,使其打开即处于登录状态。
鉴权流程图
flowchart TD
A[用户] --> B[小程序端]
B --> C[授权前置页面,码和内容我方提供]
C -->|允许授权| D[登录成功,跳转,根据分享码计算]
C -->|拒绝授权| X[停止授权报错提示页面]
核心流程:
- 用户进入小程序登录页,携带直播间跳转参数:参数
alias与roomId等 (相关参数联系我方技术人员获取); - 小程序端调用微信
uni.login获取临时code; - 小程序端调用平台接口,用
code和小程序应用标识换取OpenId、UnionId; - 小程序端上报用户信息,换取平台方
Token与ExpiresAt; - 小程序端打开
web-view,加载 H5 直播间,并在 URL 中追加token、expires_at。
sequenceDiagram
participant User as 用户
participant Mini as 小程序端
participant Wechat as 微信
participant Server as 平台方 Lemon 接口
participant H5 as 平台方 H5
User->>Mini: 打开登录页,携带 alias / roomId
Mini->>Wechat: uni.login 获取 code
Wechat-->>Mini: 返回 code
Mini->>Server: GET /sso/oauth/callback/wechat_mini
Server-->>Mini: 返回 OpenId / UnionId
Mini->>Server: PUT /sso/oauth/callback/wechat_mini/user_info
Server-->>Mini: 返回 Token / ExpiresAt
Mini->>H5: web-view 加载 H5 URL,追加 token / expires_at
H5-->>User: 展示已登录直播间
2. 接入准备
| 配置项 | 示例 | 说明 |
|---|---|---|
MiniProgram_APP_NAME |
mini_xxx |
平台方分配的小程序应用标识,联系平台方获取 |
| H5 基础域名 | https://example.xxx.com |
平台方 H5 直播间域名 |
小程序端还需要确认:
- 小程序已具备微信登录能力,可调用
uni.login({ provider: 'weixin' }); - 小程序已配置合法
web-view业务域名; - 小程序中存在登录页与 H5 承载页:
- 登录页:参考实现为
/pages/transit/index,可按项目实际路由自定义; - H5 承载页:参考实现为
/pages/live/webview,可按项目实际路由自定义。
3. 入口参数规则
登录页负责接收直播间参数,解析后组装 H5 直播间路径。
3.1 普通页面路径
/pages/transit/index?a={alias}&r={roomId}&inviteId={inviteId}
| 参数 | 必填 | 说明 |
|---|---|---|
a / alias |
是 | 企业或直播间别名 |
r / roomId |
是 | 直播间 ID |
inviteId |
否 | 邀请人 ID(字符串),用于 H5 继续透传 |
示例:
/pages/transit/index?a=demo&r=10001&inviteId=888
登录页解析后会生成 H5 相对路径:
/{alias}/{roomId}
/{alias}/{roomId}?inviteId={inviteId}
4. 登录页登录流程与接口对应关系
参考实现中,登录页由 pages/transit/index.vue 负责页面状态与路由处理,登录相关接口统一封装在 pages/transit/composables/useTransitAuth.js 中。流程和接口对应关系如下:
| 流程步骤 | 参考方法/组件 | 对应接口 | 成功处理 | 失败处理 |
|---|---|---|---|---|
| 解析入口参数 | initTransit |
无 | 得到 alias、roomId、inviteId,生成 targetPath |
参数缺失时展示错误 |
| 判断是否可静默登录 | checkSilentLogin |
无 | lastTransitParams.alias、roomId 与本次一致时尝试静默登录 |
不一致时展示手动登录表单 |
| 静默登录 | trySilentLogin |
6.1 微信 code 换 OpenId / UnionId、6.2 上报用户信息并换取 Token | 缓存 lemon_token、lemon_expires_at 后进入 H5 |
返回 false,降级展示手动登录表单 |
| 手动登录表单校验 | LoginForm.vue |
无 | 头像、昵称、协议均满足后触发 login 事件 |
提示用户补全头像/昵称或同意协议 |
| 手动登录 | handleManualLogin |
6.1、6.2、6.3 上传微信临时头像 | 换取 Token,必要时上传头像并二次更新用户信息 | 提示“登录失败,请重试”,停留在登录表单 |
| 记录参数并进入 H5 | recordTransitParams、navigateToLive |
无 | 缓存 lastTransitParams,跳转 /pages/live/webview |
跳转失败时由小程序路由或 web-view 错误处理 |
4.1 参数解析与 H5 路径生成
登录页加载时执行 initTransit(options):
- 读取
options.a/options.alias与options.r/options.roomId; - 读取可选参数
options.inviteId; alias或roomId缺失时,展示“参数缺失,无法进入直播间”;- 解析成功后生成
targetPath。
const alias = options.a || options.alias || '';
const roomId = options.r || options.roomId || '';
const inviteId = options.inviteId || '';
let path = '/' + alias + '/' + roomId;
if (inviteId) {
path += '?inviteId=' + inviteId;
}
targetPath.value = path;
4.2 静默登录判断与执行
登录页使用 checkSilentLogin(alias, roomId) 判断是否尝试静默登录。该方法只比较缓存的 lastTransitParams,不额外判断 Token 是否过期。
缓存结构:
{
"alias": "demo",
"roomId": "10001",
"timestamp": 1919202030300
}
当 alias、roomId 与上次一致时,执行 trySilentLogin():
- 调用
uni.login({ provider: 'weixin' })获取微信临时code; - 调用 6.1 微信 code 换 OpenId / UnionId;
- 调用 6.2 上报用户信息并换取 Token,仅提交
App、OpenID、UnionID; - 将返回的
Token、ExpiresAt写入lemon_token、lemon_expires_at; - 调用
recordTransitParams(alias, roomId),再通过navigateToLive(targetPath)进入 H5。
静默登录任一步骤异常时返回 false,页面切换到手动登录表单。
4.3 手动登录与头像更新
首次进入、上次进入参数不一致,或静默登录失败时展示 LoginForm.vue。表单负责收集:
open-type="chooseAvatar"返回的avatarUrl;type="nickname"输入的昵称;- 用户协议和隐私政策勾选状态。
用户点击登录后,登录页执行 handleManualLogin(userInfo):
- 调用
uni.login获取微信临时code; - 调用 6.1 微信 code 换 OpenId / UnionId;
- 调用 6.2 上报用户信息并换取 Token,首次提交
App、Nickname、OpenID、UnionID; - 从返回结果中取
Token、ExpiresAt; - 若头像地址为微信临时文件路径,调用 6.3 上传微信临时头像;
- 头像上传成功后,再次调用 6.2,补充
Avatar更新用户资料; - 缓存
lemon_token、lemon_expires_at; - 登录成功后记录
lastTransitParams,再跳转 H5。
5. H5 承载页行为
小程序端获取 Token 后,跳转到:
/pages/live/webview?url={encodedH5Url}&token={token}&expires_at={expiresAt}
其中:
encodedH5Url是encodeURIComponent后的 H5 直播间 URL;- 参考实现中 H5 URL 形如:
https://example.xxx.com/{alias}/{roomId}; - 如果存在
inviteId,H5 URL 形如:https://example.xxx.com/{alias}/{roomId}?inviteId={inviteId}。
H5 承载页处理规则:
- 读取
url参数并执行decodeURIComponent; - 校验 URL 必须以
http://或https://开头; - 将除
url以外的参数全部追加到 H5 URL; - 最终由
web-view加载完整 URL。
最终加载示例:
https://example.xxx.com/demo/10001?inviteId=888&token=xxxx&expires_at=1919202090
注意:分享路径不要直接使用带
token的 H5 URL。参考实现中,分享时会根据lastTransitParams重新生成/pages/transit/index?a={alias}&r={roomId},让被分享用户重新走登录页登录流程。
6. 接口定义
下列接口编号与第 4 节流程表对应。参考实现中接口统一封装在 sheep/api/live/sso.js。
6.1 微信 code 换 OpenId / UnionId
- 流程位置:静默登录
trySilentLogin与手动登录handleManualLogin - 调用方:小程序端 → 平台方 Lemon 接口
- 请求方式:
GET - 接口路径:
GET /sso/oauth/callback/wechat_mini?code={code}&app={app}
请求参数:
| 参数 | 必填 | 说明 |
|---|---|---|
code |
是 | 微信 uni.login 返回的临时登录凭证 |
app |
是 | 平台方分配的小程序应用标识,对应 MiniProgram_APP_NAME |
请求示例:
curl "https://api.gouboyun.tv/lemon/sso/oauth/callback/wechat_mini?code=wx_code_xxx&app=mini_xxx"
成功响应示例:
{
"Code": 0,
"Message": "Success",
"Error": "",
"Data": {
"OpenId": "o_xxxxxxxx",
"UnionId": "u_xxxxxxxx"
}
}
| 字段 | 类型 | 说明 |
|---|---|---|
Data.OpenId |
String | 当前微信小程序用户 OpenId |
Data.UnionId |
String | 微信开放平台 UnionId,可能为空 |
参考封装:
export function getSsoOauthCallbackWechatMini(code, app) {
return new Promise((resolve, reject) => {
uni.request({
url: `${baseURL}/sso/oauth/callback/wechat_mini`,
method: 'GET',
data: { code, app },
success: (res) => {
resolve(res.data.Data);
},
fail: (err) => {
reject(err);
},
});
});
}
6.2 上报用户信息并换取 Token
- 流程位置:静默登录
trySilentLogin与手动登录handleManualLogin - 调用方:小程序端 → 平台方 Lemon 接口
- 请求方式:
PUT - Content-Type:
application/json - 接口路径:
PUT /sso/oauth/callback/wechat_mini/user_info
静默登录请求示例:
{
"App": "mini_xxx",
"OpenID": "o_xxxxxxxx",
"UnionID": "u_xxxxxxxx"
}
手动登录首次请求示例:
{
"App": "mini_xxx",
"Nickname": "张三",
"OpenID": "o_xxxxxxxx",
"UnionID": "u_xxxxxxxx"
}
头像上传成功后二次更新示例:
{
"App": "mini_xxx",
"Nickname": "张三",
"OpenID": "o_xxxxxxxx",
"UnionID": "u_xxxxxxxx",
"Avatar": "https://example.com/avatar.png"
}
请求字段:
| 字段 | 必填 | 说明 |
|---|---|---|
App |
是 | 平台方分配的小程序应用标识 |
OpenID |
是 | 微信小程序用户 OpenId |
UnionID |
否 | 微信开放平台 UnionId,没有时传空字符串 |
Nickname |
否 | 用户昵称,手动登录时传入 |
Avatar |
否 | 用户头像 URL,头像上传成功后再次调用该接口更新 |
成功响应示例:
{
"Code": 0,
"Message": "Success",
"Error": "",
"Data": {
"Token": "eyJhbGciOi...",
"ExpiresAt": 1919202090
}
}
| 字段 | 类型 | 说明 |
|---|---|---|
Data.Token |
String | 平台方 H5 登录令牌 |
Data.ExpiresAt |
Integer | Token 过期时间(Unix 秒级时间戳) |
小程序端应将返回值写入本地缓存:
// 缓存平台方 H5 登录态,后续跳转 web-view 时透传
uni.setStorageSync('lemon_token', data.Token);
uni.setStorageSync('lemon_expires_at', data.ExpiresAt);
参考封装:
export function putSsoOauthCallbackWechatMiniUserInfo(data) {
return new Promise((resolve, reject) => {
uni.request({
url: `${baseURL}/sso/oauth/callback/wechat_mini/user_info`,
method: 'PUT',
data,
success: (res) => {
resolve(res.data.Data);
},
fail: (err) => {
reject(err);
},
});
});
}
6.3 上传微信临时头像(辅助接口)
- 流程位置:手动登录
handleManualLogin获取Token后,且头像为临时路径时调用 - 调用方:小程序端 → 平台方 Lemon 接口
- 请求方式:
POST - 接口路径:
POST /global/upload/avatar
请求要求:
| 项 | 说明 |
|---|---|
| 文件字段名 | file |
| Header | Authorization: Bearer {Token} |
| 使用场景 | 用户选择的头像为 wxfile:// 或 http(s)://tmp/ 临时路径时上传 |
成功响应示例:
{
"Code": 0,
"Message": "Success",
"Error": "",
"Data": "https://example.com/avatar.png"
}
上传成功后,使用返回的 Data 作为 Avatar,再次调用 6.2 上报用户信息并换取 Token 更新头像。
头像上传接口封装:
export function uploadAvatar(filePath, token) {
return new Promise((resolve, reject) => {
uni.uploadFile({
url: `${baseURL}/global/upload/avatar`,
filePath,
name: 'file',
header: {
Authorization: `Bearer ${token}`,
},
success: (res) => {
const data = JSON.parse(res.data);
resolve(data.Data);
},
fail: (err) => {
reject(err);
},
});
});
}
手动登录中头像上传与二次更新的完整顺序:
async function handleManualLogin(userInfo) {
try {
const loginRes = await new Promise((resolve, reject) => {
uni.login({
provider: 'weixin',
success: resolve,
fail: reject,
});
});
if (loginRes.errMsg !== 'login:ok' || !loginRes.code) {
uni.showToast({ title: '微信登录失败', icon: 'none' });
return false;
}
// 第一步:微信 code 换取 OpenId / UnionId
const { OpenId, UnionId } = await getSsoOauthCallbackWechatMini(
loginRes.code,
getAppName(),
);
// 第二步:先用昵称和微信身份换取平台方 Token
const data = await putSsoOauthCallbackWechatMiniUserInfo({
App: getAppName(),
Nickname: userInfo.nickname,
OpenID: OpenId,
UnionID: UnionId || '',
});
const token = data.Token;
const expiresAt = data.ExpiresAt;
let avatarUrl = userInfo.avatarUrl;
// 第三步:微信 chooseAvatar 返回临时路径时,先上传为平台可访问的头像 URL
const isLocalAvatar =
/^wxfile:\/\//.test(avatarUrl) || /^https?:\/\/tmp\//.test(avatarUrl);
if (isLocalAvatar && token) {
try {
const uploadData = await uploadAvatar(avatarUrl, token);
if (uploadData) {
avatarUrl = uploadData;
// 第四步:头像上传成功后,二次更新用户资料中的 Avatar
await putSsoOauthCallbackWechatMiniUserInfo({
App: getAppName(),
Nickname: userInfo.nickname,
OpenID: OpenId,
UnionID: UnionId || '',
Avatar: avatarUrl,
});
}
} catch (e) {
// 参考实现中头像上传失败不阻断登录,只记录错误
console.error('[handleManualLogin] 头像上传失败:', e);
}
}
// 第五步:缓存 Token,供跳转 web-view 时透传给 H5
uni.setStorageSync('lemon_token', token);
uni.setStorageSync('lemon_expires_at', expiresAt);
return true;
} catch (e) {
console.error('[handleManualLogin] 手动登录失败:', e);
uni.showToast({ title: '登录失败,请重试', icon: 'none' });
return false;
}
}
6.4 错误响应处理
所有接口响应均包含以下公共字段:
| 字段 | 说明 |
|---|---|
Code |
0 表示成功,非 0 表示失败 |
Message |
结果描述 |
Error |
错误详情,成功时为空字符串 |
常见错误场景:
| 场景 | 可能原因 | 建议处理 |
|---|---|---|
code 失效 |
微信临时登录凭证已过期(有效期约 5 分钟) | 重新调用 uni.login 获取新 code |
app 标识无效 |
小程序应用标识未在平台方配置 | 联系平台方确认 MiniProgram_APP_NAME |
| 上传文件失败 | 微信临时头像无法上传或服务端异常 | 参考实现中不阻断登录,仅记录错误 |
| 服务端错误 | 平台方服务异常 | 提示用户稍后重试 |
7. 分享与重新进入
H5 承载页支持小程序分享。分享时应回到登录页,而不是分享当前 web-view 的完整 H5 地址。
推荐分享路径:
/pages/transit/index?a={alias}&r={roomId}
处理原因:
token属于当前用户登录态,不应通过分享链接外泄;- 被分享用户需要重新调用微信登录,生成自己的
OpenId、UnionId与Token; - 回到登录页后,可以复用同一套参数校验、静默/手动登录流程。
8. 异常处理
| 场景 | 建议提示/处理 |
|---|---|
缺少 alias 或 roomId |
提示“参数缺失,无法进入直播间” |
uni.login 失败 |
提示“微信登录失败” |
| 静默登录失败 | 降级展示手动登录表单 |
| 用户未填写头像或昵称 | 提示“请完善头像和昵称” |
| 用户未同意协议 | 提示“请先同意用户协议和隐私政策” |
| H5 URL 缺失 | 提示“缺少页面链接参数” |
H5 URL 非 http/https |
提示“页面链接格式不正确” |
web-view 加载失败 |
提示“页面加载失败,请稍后重试” |
9. 接入验收清单
- 小程序可通过普通路径进入登录页,并正确解析
alias、roomId、inviteId; - 同一直播间二次进入时可尝试静默登录;
- 首次进入或静默登录失败时可展示头像、昵称、协议确认表单;
- 登录成功后能缓存
lemon_token、lemon_expires_at; - 手动登录时头像能成功上传并更新;
- H5 承载页最终 URL 包含
token、expires_at; - 分享路径回到
/pages/transit/index,不包含token; -
web-view业务域名已在微信小程序后台配置。
附录:关键跳转代码示例
// 登录成功后调用,targetPath 为登录页解析生成的 H5 相对路径,如 /demo/10001
function navigateToLive(targetPath) {
const token = uni.getStorageSync('lemon_token');
const expiresAt = uni.getStorageSync('lemon_expires_at');
const baseUrl = 'https://example.xxx.com';
const livePath = baseUrl + targetPath;
// 将 H5 地址编码后传给 web-view 承载页,同时透传登录态
uni.redirectTo({
url: `/pages/live/webview?url=${encodeURIComponent(livePath)}&token=${token}&expires_at=${expiresAt}`,
});
}
function buildUrlWithParams(baseUrl, params) {
const validParams = [];
for (const key in params) {
const value = params[key];
if (value !== undefined && value !== '') {
validParams.push(key + '=' + encodeURIComponent(value));
}
}
if (validParams.length === 0) {
return baseUrl;
}
const queryString = validParams.join('&');
return baseUrl.indexOf('?') !== -1
? baseUrl + '&' + queryString
: baseUrl + '?' + queryString;
}