Skip to content

QQ 登录互通能力(qq-mp)

微信小程序/小游戏 ⇄ 腾讯 QQ 小程序的登录互通封装。

零业务耦合,所有外部依赖(接口域名、post 方法、QQ 互联 AppId、storage、toast、玩家服务等)均由调用方注入。

文件结构

qq-mp/
├── index.ts              统一导出
├── types.ts              完整类型定义
├── qq-mp.ts              核心:登录跳转 / 票据提取 / 换取登录态
├── qq-mini-plugin.ts     qq-wxmini-plugin 封装(QQ 用户访问微信小程序)
├── AccountService.ts     业务编排单例(切号入口 / wx.onLaunch / wx.onShow)
└── config.ts             ⚠️ 仅作示例的常量文件,t-comm 内部不引用

文档参考


一、微信 ⇄ QQ 登录切换

1. 跳转 QQ 小程序登录

ts
import { launchQQMP } from 'module/auth/qq-mp';

launchQQMP({
  qqAppId: 123456,                  // 必填:QQ 互联 AppId
  // qqMpAppId: 'wx26da53d900421226',   // 可选,默认值即此
  // loginPath: 'pagesLogin/pages/login/login', // 可选
  // forcePwd: false,                   // 可选,是否强制密码
  // retcodes: [],                      // 可选,回传 QQ 小程序的状态码
  // envVersion: 'trial',               // 可选,体验版调试用
}).then(() => {
  // 跳转成功
}).catch((err) => {
  console.error(err);
});

2. App.onShow 中提取票据

ts
import { handleQQLoginOnShow } from 'module/auth/qq-mp';

// 微信小游戏入口(GooseHomeApp.ts 或 App.vue 等)
wx.onShow((options) => {
  const ticket = handleQQLoginOnShow(options);
  // ticket 含:qqOpenid / qqAccessToken / qqRefreshToken / qqAppid
  if (!ticket) return;

  // 调换取登录态接口(见下一节)
});

handleQQLoginOnShow 默认会用 wx.setStorageSync 缓存票据,如需自定义存储:

ts
import { handleQQLoginOnShow, QQ_TICKET_STORAGE_KEY } from 'module/auth/qq-mp';
import { oops } from 'core/Oops';

handleQQLoginOnShow(options, {
  storage: {
    set: (k, v) => oops.storage.set(k, v),
    get: (k) => oops.storage.get(k),
    remove: (k) => oops.storage.remove(k),
  },
  // storageKey: 'MY_QQ_TICKET',
});

3. 用票据换取登录态

ts
import { queryQQLoginUserInfo } from 'module/auth/qq-mp';
import { post } from 't-comm/es/cocos/manager/index.mjs';
import { isTestEnv } from 't-comm/es/cocos/env/index.mjs';

const res = queryQQLoginUserInfo({
  // 必填:接口域名(支持函数动态返回)
  host: () => isTestEnv() ? 'https://atest.igame.qq.com' : 'https://a.igame.qq.com',
  // 必填:post 方法
  post,
  // 必填:票据信息
  ticketInfo: ticket,
  // 可选:兜底 QQ 互联 AppId(票据中无 appid 时使用)
  fallbackQQAppId: 123456,
  // 可选:接口路径,默认 pmdtrpc.commcgi.user.user/QueryUserInfo
  // apiPath: '...',
  // 可选:额外 query/reqData
  // extraQuery: {},
  // extraReqData: {},
});

res.then((data) => {
  if (data?.user_info) {
    console.log('切换 QQ 登录成功', data.user_info);
  }
});

4. 其它工具

ts
import { readQQTicketInfo, clearQQTicketInfo } from 'module/auth/qq-mp';

const ticket = readQQTicketInfo();   // 从 storage 读已保存的票据
clearQQTicketInfo();                  // 清除票据

二、QQ 用户访问微信小程序(qq-wxmini-plugin)

1. 配置插件

manifest.jsonmp-weixin 节点:

json
{
  "mp-weixin": {
    "plugins": {
      "qq-wxmini-plugin": {
        "version": "1.0.7",
        "provider": "wx41f6784ad4052201"
      }
    }
  }
}

⚠️ 使用前需先在微信公众平台后台添加该插件的授权

2. 入口初始化与环境检测

ts
import {
  initQQMiniPlugin,
  checkIsQQEnv,
  clearWxLoginStorageIfQQEnv,
} from 'module/auth/qq-mp';

// onLaunch
initQQMiniPlugin();

// QQ 环境下若误用了微信登录态,清除 storage 重新走 QQ 登录
clearWxLoginStorageIfQQEnv({
  isWxLoggedIn: () => {
    // 业务自定义判断逻辑(如 cookie tip_utype === '2')
    return /* ... */ false;
  },
  // clearStorage: () => oops.storage.removeAll?.(),  // 可选
});

// 任意位置使用
if (checkIsQQEnv()) {
  // 当前是 QQ 用户通过插件访问
}

三、零耦合说明

本目录不依赖任何业务包

外部依赖注入方式
接口域名queryQQLoginUserInfo({ host })(支持字符串或函数)
post 请求方法queryQQLoginUserInfo({ post })
QQ 互联 AppIdlaunchQQMP({ qqAppId })
storagehandleQQLoginOnShow(_, { storage }) 可选
微信登录态判断clearWxLoginStorageIfQQEnv({ isWxLoggedIn })

仅依赖:

  • wx.* 小程序/小游戏全局 API
  • requirePlugin(小程序内置全局)

四、AccountService —— 高层业务编排(可选)

如果你不想自己拼装上面这些原子能力,可以直接用 AccountService 单例来完成 「微信 ⇄ QQ 登录态切换 + 启动期初始化」。它在内部组合调用了 launchQQMP / handleQQLoginOnShow / queryQQLoginUserInfo / clearQQTicketInfo / clearWxLoginStorageIfQQEnv / initQQMiniPlugin,并管理切号过程中的状态。

AccountService 同样零业务耦合,所有依赖通过 configure() 一次性注入。

1. 启动期注入依赖

ts
import { AccountService } from 't-comm/es/qq-mp';

import { isQQAccount } from '@/api/account';
import { config as apiConfig } from '@/api/config';
import { oops } from '@/core/Oops';
import { ToastTip } from '@/core/render/ToastTip';
import { PlayerService } from '@/module/player/service/PlayerService';
import { isTestEnv } from 't-comm/es/cocos/env/index.mjs';
import { post } from 't-comm/es/cocos/manager/index.mjs';

const QQ_APP_ID = 123456; // 业务方自己的 QQ 互联 AppId

AccountService.configure({
  qqAppId: QQ_APP_ID,
  post,
  getQQLoginUserInfoHost: () => isTestEnv()
    ? 'https://atest.igame.qq.com'
    : 'https://a.igame.qq.com',
  storage: {
    set:    (k, v) => oops.storage.set(k, v),
    get:    <T = any>(k: string) => oops.storage.get<T>(k),
    remove: (k) => oops.storage.remove(k),
  },
  loginInfoStorageKey: apiConfig.loginInfoStorageKey,
  isQQAccount,                                // () => boolean
  toast:           (msg) => ToastTip.show(msg),
  clearPlayer:     () => PlayerService.ins.clear(),
  bootstrapPlayer: (opts) => PlayerService.ins.bootstrap(opts),
});

2. 启动期入口

ts
// onLaunch / Game.bootstrap
AccountService.ins.handleAppOnLaunch();

内部会执行:

  • initQQMiniPlugin():初始化 qq-wxmini-plugin
  • 若处于 QQ 环境但本地却是微信登录态,自动清空 loginInfoStorageKey
  • 绑定 wx.onShow:从腾讯 QQ 小程序返回时自动提取票据 → 调 queryQQLoginUserInfo 换登录态 → 调 clearPlayer + bootstrapPlayer({ force:true })

3. 设置页切号

ts
// 「切换 QQ 账号 / 切换微信账号」按钮文案
const text = AccountService.ins.getSwitchButtonText();

// 切到 QQ 账号
AccountService.ins.switchToQQ();

// 切回微信账号
AccountService.ins.switchToWx();

4. 自定义文案(可选)

configure 的第二个参数可覆盖默认中文文案:

ts
AccountService.configure(deps, {
  alreadyQQ:       'Already on QQ account',
  alreadyWx:       'Already on WeChat account',
  switchQQFail:    'Switch to QQ failed, please retry',
  switchQQRetry:   'Switch to QQ failed, please try again',
  switchQQSuccess: 'Switched to QQ account',
  switchWxSuccess: 'Switched to WeChat account',
  switchWxFail:    'Switch to WeChat failed, please retry',
});

5. 完整依赖一览

依赖项类型说明
qqAppIdstring | numberQQ 互联 AppId
postQQPostFn通用 post 方法(用于调 QueryUserInfo)
getQQLoginUserInfoHoststring | () => stringQueryUserInfo 域名(可为函数以便区分测试 / 正式环境)
storageQQStorageLikestorage 适配器(一般桥接到 oops.storage)
loginInfoStorageKeystringloginInfo 在 storage 中的 key
isQQAccount() => boolean当前是否为 QQ 账号
toast(msg: string) => void显示提示文案
clearPlayer() => void清空玩家本地缓存
bootstrapPlayer(opts: { force: boolean }) => Promise | void强制重新初始化玩家服务

未调用 AccountService.configure(deps) 就使用 AccountService.ins.* 时会 抛出 [AccountService] 未初始化 错误,便于排错。


五、QQ App 环境直登路径(推荐 cocos-game 接入)

适用场景:用户在 QQ App 中通过 qq-wxmini-plugin 访问微信小游戏(即 checkIsQQEnv() === true)。 此时无需wx.navigateToMiniProgram 跳转腾讯 QQ 小程序,可直接:

  • 切 QQ 账号plugin.login() 拿 QQ code → 业务后台 _ltype=tiploginqqproc 换登录态
  • 切微信账号wx.login() 拿微信 code → 业务后台 _ltype=tiploginwxproc 换登录态

文档参考:QQ-WXMINI-PLUGIN

1. 新增的原子能力

能力说明
qqPluginLogin()QQ App 环境下,通过 plugin.login() 同步拿到 QQ code
wxLogin()通过 wx.login() 拿到微信 code(QQ App 下也可用)
clearWxLoginStorageIfQQEnv({ shouldForceQQ })新增 shouldForceQQ 开关,业务可控制启动期是否强制 QQ 登录
loginMp({ code, getCode, _ltype })cocos/loginloginMp 现已原生支持 QQ 登录:传 _ltype: 'tiploginqqproc' 配合 codegetCode 即可
ts
import { qqPluginLogin, wxLogin } from 't-comm/es/qq-mp';

const { code: qqCode } = await qqPluginLogin();   // QQ App 下拿 QQ code
const { code: wxCode } = await wxLogin();          // 任意环境拿微信 code

2. AccountService 新增三个可选依赖

依赖项类型说明
shouldForceQQ() => boolean(可选)启动期是否强制 QQ 登录态,默认 () => true(保留旧行为)。
若希望「QQ App 下也允许手动切到微信账号」,传 () => false,或基于业务自身的「用户是否手动选了微信」状态返回值
code2QQLogin(code: string) => Promise | void(可选)QQ App 下「切 QQ 账号」的后台探身入口。AccountService 会先用 qqPluginLogin() 拿 code 再交给它。不传则 fallback 到旧的 launchQQMP 跳转路径
code2WxLogin(code: string) => Promise | void(可选)QQ App 下「切微信账号」的后台探身入口。AccountService 会先用 wxLogin() 拿 code 再交给它。不传则 fallback 到旧的「清 storage + bootstrapPlayer」路径

三个依赖全部可选,未传时 AccountService 行为与改动前完全一致

3. switchToQQ / switchToWx 的环境分流

text
switchToQQ()
  ├── checkIsQQEnv() === true  且传了 code2QQLogin
  │     → qqPluginLogin() 拿 QQ code → code2QQLogin(code) → bootstrapPlayer({ force:true })
  └── 否则(微信宿主 / 未传 code2QQLogin)
        → launchQQMP() 跳腾讯 QQ 小程序 → wx.onShow 回到宿主 → 旧票据流程

switchToWx()
  ├── checkIsQQEnv() === true  且传了 code2WxLogin
  │     → wxLogin() 拿微信 code → code2WxLogin(code) → bootstrapPlayer({ force:true })
  └── 否则(微信宿主 / 未传 code2WxLogin)
        → 清 QQ 票据 + loginInfo → bootstrapPlayer({ force:true })

六、cocos-game 业务方接入改动指引

cocos-game 业务方在 AccountService.configure()追加以下三个依赖即可(其它字段保持不变)。

业务后台 code 换登录态使用 t-comm 已有的 loginMp_ltype=tiploginqqproc / _ltype=tiploginwxproc)。

1. 引入

ts
import { AccountService } from 't-comm/es/qq-mp';
import { loginMp } from 't-comm/es/cocos/login/login';

2. loginMp 已原生支持 QQ 登录

loginMp 现已支持两个可选参数,同时支持微信登录和 QQ 登录

参数类型说明
codestring(可选)外部已拿到的 code(如 plugin.login() 拿到的 QQ code)。传了就跳过内部的 wx.login()
getCode() => Promise<{ code: string }>(可选)自定义 code 提供者。例如直接传入 qqPluginLogin

code 来源优先级:code > getCode > 默认 wx.login()

ts
// 微信登录(默认行为,向后兼容)
loginMp({ url, appid });

// QQ 登录(QQ App 环境下,已有 plugin.login() 拿到的 QQ code)
loginMp({ url, appid, _ltype: 'tiploginqqproc', code: qqCode });

// QQ 登录(让 loginMp 自己去调 plugin.login)
import { qqPluginLogin } from 't-comm/es/qq-mp';
loginMp({ url, appid, _ltype: 'tiploginqqproc', getCode: qqPluginLogin });

3. configure 追加字段

ts
AccountService.configure({
  // ===== 原有字段保持不变 =====
  qqAppId: QQ_APP_ID,
  post,
  getQQLoginUserInfoHost: () => isTestEnv()
    ? 'https://atest.igame.qq.com'
    : 'https://a.igame.qq.com',
  storage,
  loginInfoStorageKey: apiConfig.loginInfoStorageKey,
  isQQAccount,
  toast: msg => ToastTip.show(msg),
  clearPlayer: () => PlayerService.ins.clear(),
  bootstrapPlayer: opts => PlayerService.ins.bootstrap(opts),

  // ===== 【新增】=====

  // ① 是否「QQ App 下强制 QQ 登录态」
  //    业务希望支持 QQ App 下手动切微信时,基于业务的「用户已手动切到微信」状态返回值
  //    例如以 storage 记一个 user_picked_wx 标记
  shouldForceQQ: () => !storage.get('user_picked_wx'),

  // ② QQ App 下「切 QQ 账号」走业务后台 _ltype=tiploginqqproc
  //    AccountService 会先用 plugin.login() 拿 QQ code,再调用此函数
  //    业务侧只需把 code 透传给 loginMp 的 code 参数即可
  code2QQLogin: (code) => loginMp({
    url: getApiHost() + '/login',
    appid: WX_APP_ID,
    _ltype: 'tiploginqqproc',
    code,                                  // ← 直接用 plugin.login() 拿到的 QQ code
    storage,
    storageKey: apiConfig.loginInfoStorageKey,
    onLoginInfo: (info) => {
      storage.remove('user_picked_wx');    // 切回 QQ 时清掉微信偏好
      updateLocalLoginInfo(info);
    },
  }),

  // ③ QQ App 下「切微信账号」走业务后台 _ltype=tiploginwxproc
  //    AccountService 会先用 wx.login() 拿微信 code,再调用此函数
  code2WxLogin: (code) => loginMp({
    url: getApiHost() + '/login',
    appid: WX_APP_ID,
    _ltype: 'tiploginwxproc',
    code,                                  // ← 直接用 wx.login() 拿到的微信 code
    storage,
    storageKey: apiConfig.loginInfoStorageKey,
    onLoginInfo: (info) => {
      storage.set('user_picked_wx', true); // 记下用户主动选了微信
      updateLocalLoginInfo(info);
    },
  }),
});

4. 切号 UI 接入

切号按钮无需任何改动,依然调用:

ts
AccountService.ins.switchToQQ();   // → QQ App 下走 plugin.login + code2QQLogin
AccountService.ins.switchToWx();   // → QQ App 下走 wx.login   + code2WxLogin

AccountService 内部会根据 checkIsQQEnv() 自动分流到「直登路径」或「跳腾讯 QQ 小程序路径」。

5. 兼容性与回滚

  • 不传 shouldForceQQ / code2QQLogin / code2WxLogin:行为与改动前完全一致
  • 现有所有 qq-mp 单测(28 + 25 + 27)全部通过,无破坏性变更