docs: add SDK设计规范.md — 全平台 SDK 架构需求文档

涵盖:
- XuqmSDK 仅两种初始化方式(配置文件自动/手动 initialize),移除 initializeFromLicense 和 init()
- setUserInfo 作为全局用户认证枢纽,一次调用自动同步 Push/IM/License/Update
- PushSDK 无需独立初始化,由 setUserInfo 触发全自动设备注册
- ImSDK login 通过 setUserInfo.userSig 驱动;新增 refreshToken
- UpdateSDK:批量注册插件、版本号自动获取、Android APK 直接安装、两步插件更新 API
- LicenseSDK:无独立初始化,依赖 XuqmSDK 公共上下文
- 全平台通用要求:RN/Android/iOS/Flutter/HarmonyOS/小程序/Vue3/Server SDK

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
XuqmGroup 2026-06-15 10:32:09 +08:00
父节点 90ba1f9de3
当前提交 d86cab56b3

539
docs/SDK设计规范.md 普通文件
查看文件

@ -0,0 +1,539 @@
# XuqmGroup SDK 设计规范
> 版本v2.0
> 生效范围:**全平台** — Android、iOS、React Native、Flutter、HarmonyOS、微信小程序、Vue3、Java / Python / Go 服务端 SDK
> 状态:需求确认,待各平台落地实现
---
## 一、设计原则
1. **App 零感知初始化**配置文件方式下,App 无需写任何初始化代码。
2. **一次认证,全局生效**`setUserInfo()` 一次调用,所有子 SDKPush、IM、License 等)自动同步状态。
3. **失败即失败**:初始化失败、认证失败均抛出错误,不静默降级。
4. **服务按需激活**哪些服务IM、推送、License 等由租户平台配置决定,SDK 根据返回配置自动激活对应能力,App 层无需判断。
5. **子 SDK 独立性**:各子 SDK 之间通过 `common` 共享上下文appKey、baseUrl、userId 等),不互相硬依赖。
---
## 二、XuqmSDK核心 — @xuqm/rn-common
### 2.1 初始化方式(仅两种)
#### 方式 A配置文件自动初始化推荐
App 在构建阶段将加密配置文件(`.xuqmconfig`放置到指定路径,SDK 在模块加载时自动读取并初始化。App 无需调用任何初始化代码。
```
配置文件路径RNsrc/assets/xuqm/config.xuqmconfig
配置文件格式:见 docs/配置文件规范.md
```
配置文件内容(加密前)包含:
- `appKey`:应用标识
- `platformUrl`:平台地址(从哪里拉取服务配置)
SDK 读取配置文件后,自动调用 **方式 B** 完成完整初始化(拉取远程服务配置)。
---
#### 方式 B手动初始化
App 主动调用,适用于无法提前写入配置文件或需要动态切换环境的场景。
```ts
await XuqmSDK.initialize({
appKey: 'your-app-key',
platformUrl: 'https://www.51szyx.com', // 平台地址,必填
debug: __DEV__,
})
```
**行为**
1. 请求 `{platformUrl}/api/sdk/config?appKey={appKey}`
2. 平台根据 appKey 和租户服务开通情况,返回该 App 专属的服务配置:
```json
{
"apiUrl": "...", // 通用 API 地址
"imWsUrl": "...", // IM WebSocket 地址(未开通则为 null
"fileServiceUrl": "...", // 文件服务地址
"pushEnabled": true, // 是否开通推送服务
"licenseEnabled": false, // 是否开通 License 服务
"imEnabled": true // 是否开通 IM 服务
}
```
3. SDK 将服务配置保存,供所有子 SDK 使用。
4. **失败时直接抛出错误**,不降级,不回退到默认地址。调用方必须处理异常。
**签名**
```ts
interface XuqmInitOptions {
appKey: string
platformUrl: string // 平台地址,必填
debug?: boolean
}
XuqmSDK.initialize(options: XuqmInitOptions): Promise<void>
```
---
#### 已移除(不再提供)
| 方法 | 移除原因 |
|------|---------|
| `XuqmSDK.initializeFromLicense(file, options?)` | License 是独立小服务,不承担 SDK 初始化职责 |
| `XuqmSDK.init(options): void` | 同步 init 不拉取服务配置,行为不完整;用方式 A 或方式 B 替代 |
| `XuqmSDK.initWithConfigFile(encrypted)` | 由自动初始化机制内部调用,不对外暴露 |
---
### 2.2 用户认证setUserInfo核心枢纽
`setUserInfo` 是所有子 SDK 用户状态同步的入口。**登录成功后调用一次,所有子 SDK 自动激活**。
```ts
interface XuqmUserInfo {
userId: string // 必填
userSig?: string // IM 服务必填;租户未开通 IM 时可不传
name?: string
phone?: string
avatar?: string
}
XuqmSDK.setUserInfo(info: XuqmUserInfo): void
```
**调用后的内部行为(自动、静默)**
| 子 SDK | 触发动作 |
|--------|---------|
| PushSDK | 自动执行设备注册流程(见第三节) |
| ImSDK | 若 `userSig` 存在且平台开通了 IM,自动登录 IM 服务 |
| LicenseSDK | 更新用户上下文,供 License 验证使用 |
| UpdateSDK | 更新 userId,用于租户平台配置的灰度/定向更新 |
**登出时**
```ts
XuqmSDK.setUserInfo(null)
```
触发所有子 SDK 登出PushSDK 解绑 token,ImSDK 断开连接,清除所有用户上下文。
---
### 2.3 其他方法(保留)
```ts
XuqmSDK.getUserId(): string | null
XuqmSDK.getUserInfo(): XuqmUserInfo | null
XuqmSDK.awaitInitialization(): Promise<void> // 等待初始化完成
```
---
## 三、PushSDK推送 — @xuqm/rn-push
### 3.1 初始化方式
**无需显式初始化**。当 `XuqmSDK.setUserInfo(info)` 被调用时,PushSDK 自动执行以下流程:
```
setUserInfo(info)
└─ PushSDK 内部触发:
1. detectPushVendor() — 自动检测当前设备厂商(华为/小米/OPPO/vivo/荣耀/FCM/APNs
2. fetchVendorConfig(vendor) — 从平台获取该厂商推送配置AppID、Secret 等)
3. registerWithVendorSDK() — 调用厂商 SDK 完成设备注册
4. onTokenReceived(token) — 收到 token 后自动上报绑定到平台
```
App 层无需关心设备厂商类型,也无需手动调用注册流程。
### 3.2 App 层可调用的 API
```ts
// 设置离线推送开关
PushSDK.setOfflinePushEnabled(enabled: boolean): Promise<void>
// 设置免打扰时间段24小时制
PushSDK.setQuietHours(start: string, end: string): Promise<void>
// 示例PushSDK.setQuietHours('22:00', '08:00')
// 清除免打扰设置
PushSDK.clearQuietHours(): Promise<void>
// 登出通常不需要手动调用,setUserInfo(null) 会自动触发)
PushSDK.logout(): Promise<void>
```
### 3.3 已移除
| 方法 | 移除原因 |
|------|---------|
| `PushSDK.initialize(userId?)` | 由 `setUserInfo` 触发,不对外暴露 |
| `PushSDK.requestNativeRegistration()` | 内部流程,不对外暴露 |
| `PushSDK.registerToken(userId, token, vendor?)` | 内部流程,不对外暴露 |
| `PushSDK.onPushToken(callback)` | 内部流程,不对外暴露 |
| `PushSDK.unregisterToken(userId)` | 由 `setUserInfo(null)` 触发 |
| `PushSDK.setPendingToken / getPendingToken` | 内部状态,不对外暴露 |
---
## 四、ImSDK即时通讯 — @xuqm/rn-im
### 4.1 初始化与登录
**无需显式初始化**。依赖 `XuqmSDK.initialize()` 完成服务配置获取后即可工作。
**登录**:通过 `XuqmSDK.setUserInfo({ userId, userSig })` 自动触发,不需要 App 直接调用 `ImSDK.login()`
**userSig 获取方式**
- App 登录成功后,由 App 服务端根据业务逻辑生成并返回
- userSig 通常有时效性(建议 7 天,App 应在登录响应中一并返回
- App 在调用 `setUserInfo` 时传入,无需关心 IM 内部实现
```ts
// App 登录后的标准流程
const loginResult = await api.login(phone, code)
XuqmSDK.setUserInfo({
userId: loginResult.userId,
userSig: loginResult.userSig, // 服务端随登录一并返回
name: loginResult.name,
phone: loginResult.phone,
})
// → PushSDK 自动注册
// → ImSDK 自动登录(若租户开通了 IM
```
### 4.2 userSig 刷新
当 userSig 过期或需要主动刷新时:
```ts
const newSig = await api.refreshUserSig()
ImSDK.refreshToken(newSig): Promise<void>
```
或通过重新调用 `setUserInfo` 携带新 userSig。
### 4.3 IM 与 Push 共用用户上下文
Push 和 IM 共享 `XuqmSDK` 的用户上下文userId,两者通过 `setUserInfo` 统一管理:
- 同一个 userId 用于 Push 设备绑定和 IM 登录
- `setUserInfo(null)` 统一触发 Push 解绑和 IM 断连
### 4.4 可用方法
ImSDK 的具体 API 方法(消息收发、群组、好友等)保持现有设计不变,见 `docs/SDK-API参考.md` 第 5 节。
仅以下方法调整:
| 方法 | 变化 |
|------|------|
| `ImSDK.login(userId, userSig)` | 保留,但通常不需要 App 直接调用(由 setUserInfo 触发) |
| `ImSDK.refreshToken(userSig)` | **新增**,用于 userSig 过期刷新 |
| `ImSDK.disconnect()` | 保留,通常不需要直接调用setUserInfo(null) 触发) |
---
## 五、UpdateSDK更新 — @xuqm/rn-update
### 5.1 插件注册
#### 批量注册(推荐)
```ts
UpdateSDK.registerPlugins([
{ moduleId: 'buz1' },
{ moduleId: 'buz2' },
{ moduleId: 'buz3' },
])
```
**版本号由 SDK 自动获取**,不需要 App 传入:
- SDK 从本地 manifest 文件读取当前已安装的插件版本
- 如果 manifest 中无记录(首次安装),视为版本 `0.0.0`
#### 单个注册(仍支持,向后兼容)
```ts
UpdateSDK.registerPlugin({ moduleId: 'buz1' })
// 注意:不再接受 version 字段
```
**类型定义变更**
```ts
// 旧
interface PluginMeta {
moduleId: string
version: string // ← 移除
}
// 新
interface PluginRegistration {
moduleId: string
}
```
### 5.2 App 整包更新
#### 检查更新
```ts
const info = await UpdateSDK.checkAppUpdate(bypassIgnore?: boolean): Promise<AppUpdateInfo>
```
#### Android APK 直接下载安装(新增)
```ts
// 下载 APK 并在完成后调起系统安装器
await UpdateSDK.downloadAndInstallApk(
downloadUrl: string,
options?: {
onProgress?: (progress: number) => void // 0~1
sha256?: string // 校验值,有则验证
}
): Promise<void>
```
**完整整包更新流程示例**
```ts
const info = await UpdateSDK.checkAppUpdate()
if (!info.needsUpdate) return
if (Platform.OS === 'android' && info.downloadUrl) {
// Android直接下载安装
await UpdateSDK.downloadAndInstallApk(info.downloadUrl, {
onProgress: (p) => setProgress(p),
sha256: info.apkHash ?? undefined,
})
} else {
// iOS / Android 商店跳转
await UpdateSDK.openStore(info.appStoreUrl, info.marketUrl)
}
```
### 5.3 插件Bundle热更新
**两个方法,SDK 自动完成所有步骤**
#### 方法 1检查更新信息
```ts
const info = await UpdateSDK.checkPluginUpdate(moduleId: string): Promise<PluginUpdateInfo>
```
App 层拿到 `info` 后可自行决定:弹窗提示用户、静默后台更新或忽略。
```ts
interface PluginUpdateInfo {
needsUpdate: boolean
latestVersion: string
currentVersion: string // 新增SDK 自动填入
changeLog?: string
forceUpdate?: boolean // 新增:强制更新标识
minCommonVersion?: string // 要求的最低 common bundle 版本
}
```
#### 方法 2执行更新
```ts
await UpdateSDK.updatePlugin(
moduleId: string,
options?: {
onProgress?: (progress: number) => void
silent?: boolean // true = 静默更新(不重载,下次启动生效)
}
): Promise<void>
```
**SDK 内部自动完成**
1. 调用 `checkPluginUpdate()` 确认有更新
2. 下载 bundle带进度回调
3. 写入文件系统(`rn-bundles/<moduleId>.<platform>.bundle`
4. 若 `silent = false`(默认),触发宿主重载当前插件
**一步完成(静默后台更新)**
```ts
// 启动时后台静默检查并缓存,下次进入插件生效
await UpdateSDK.updatePlugin('buz1', { silent: true })
```
**带确认弹窗的前台更新**
```ts
const info = await UpdateSDK.checkPluginUpdate('buz1')
if (info.needsUpdate) {
const confirmed = info.forceUpdate || (await showConfirm(`发现新版本 ${info.latestVersion},是否更新?`))
if (confirmed) {
await UpdateSDK.updatePlugin('buz1', {
onProgress: (p) => setProgress(p),
})
}
}
```
### 5.4 已移除 / 废弃
| 方法 | 替代 |
|------|------|
| `UpdateSDK.checkAndCachePlugin(moduleId)` | `UpdateSDK.updatePlugin(moduleId, { silent: true })` |
| `UpdateSDK.downloadPluginBundle(url)` | 内部实现,不对外暴露 |
| `UpdateSDK.cachePluginBundle(...)` | 内部实现,不对外暴露 |
| `UpdateSDK.getCachedPluginBundle(moduleId)` | 内部实现,不对外暴露 |
| `PluginMeta.version`(注册时传 version| SDK 自动从 manifest 读取 |
### 5.5 用户定向更新
灰度/定向更新的用户白名单在**租户平台**配置,SDK 无需感知。UpdateSDK 在检查更新时会自动携带当前 `userId`(来自 `XuqmSDK.getUserId()`),由服务端决定是否返回 `needsUpdate: true`
---
## 六、LicenseSDKLicense 服务 — @xuqm/rn-license
### 6.1 定位
License 是**独立小服务**,大多数租户不会开通。它:
- 不承担 SDK 初始化职责
- 不向其他 SDK 提供基础能力
- 与 IM / Push / Update 等没有依赖关系
- 使用 `XuqmSDK``apiUrl`、`appKey`、`userId` 等公共上下文
### 6.2 初始化方式
**无需独立初始化**。LicenseSDK 在调用任何方法前会内部调用 `XuqmSDK.awaitInitialization()`,等待核心 SDK 就绪后直接使用其配置。
```ts
// App 只需调用 XuqmSDK.initialize(),LicenseSDK 无需额外操作
await XuqmSDK.initialize({ appKey, platformUrl })
// ...用户登录...
XuqmSDK.setUserInfo({ userId, ... })
// 然后即可直接调用 LicenseSDK 方法
const license = await LicenseSDK.validateLicense(licenseId)
```
### 6.3 已移除
| 方法 | 移除原因 |
|------|---------|
| `LicenseSDK.initialize(...)` | 依赖 XuqmSDK,无需独立初始化 |
| `XuqmSDK.initializeFromLicense(file)` | License 文件不承担 SDK 初始化职责 |
---
## 七、SDK 整体架构图
```
App
├── XuqmSDK.initialize(appKey, platformUrl)
│ └─ 拉取平台配置 ──────────────────────────────┐
│ │ 返回各服务 URL 和开通状态
│ ← apiUrl / imWsUrl / fileServiceUrl ──────────┘
├── XuqmSDK.setUserInfo({ userId, userSig?, ... })
│ ├─ PushSDK ──── 自动检测厂商 → 获取厂商配置 → 注册设备 → 上报 token
│ ├─ ImSDK ─────── 自动登录(若 userSig 存在且 IM 已开通)
│ ├─ UpdateSDK ─── 更新 userId用于定向更新
│ └─ LicenseSDK ── 更新用户上下文
└── XuqmSDK.setUserInfo(null)
├─ PushSDK ──── 解绑 token
└─ ImSDK ─────── 断开连接
```
---
## 八、各子 SDK 依赖关系
```
@xuqm/rn-common ← XuqmSDK 核心、HTTP 客户端、设备信息、公共上下文
│ 依赖(通过 awaitInitialization + getConfig + getUserId 等)
├── @xuqm/rn-push
├── @xuqm/rn-im
├── @xuqm/rn-update
├── @xuqm/rn-xwebview
└── @xuqm/rn-license
```
子 SDK 之间**不互相依赖**,均通过 `@xuqm/rn-common` 共享配置和用户上下文。
---
## 九、初始化完整流程参考
```ts
// 1. 初始化(通常在 App 入口,配置文件模式下可省略)
await XuqmSDK.initialize({
appKey: 'your-app-key',
platformUrl: 'https://www.51szyx.com',
debug: __DEV__,
})
// 2. 业务登录App 自有登录逻辑)
const result = await api.login(phone, verifyCode)
// 3. 一次调用,全局生效
XuqmSDK.setUserInfo({
userId: result.userId,
userSig: result.userSig, // 服务端随登录返回
name: result.name,
phone: result.phone,
})
// → PushSDK 自动完成设备注册
// → ImSDK 自动完成 IM 登录(若开通)
// 4. 插件热更新(后台静默,启动时调用一次)
await UpdateSDK.registerPlugins([
{ moduleId: 'buz1' },
{ moduleId: 'buz2' },
])
await Promise.all([
UpdateSDK.updatePlugin('buz1', { silent: true }),
UpdateSDK.updatePlugin('buz2', { silent: true }),
])
// 5. 登出
XuqmSDK.setUserInfo(null)
// → PushSDK 解绑,ImSDK 断连,userId 清除
```
---
## 十、跨平台实现要求
本文档中所有设计均为**跨平台通用需求**,适用于:
| 平台 | SDK 包 |
|------|--------|
| Android原生 | `xuqm-android-sdk` |
| iOS原生 | `XuqmSDK.framework` |
| React Native | `@xuqm/rn-*` |
| Flutter | `xuqm_flutter_sdk` |
| HarmonyOS | `xuqm_harmony_sdk` |
| 微信小程序 | `xuqm-miniprogram-sdk` |
| Vue3H5 | `@xuqm/vue-sdk` |
| Java 服务端 | `xuqm-java-sdk` |
| Python 服务端 | `xuqm-python-sdk` |
| Go 服务端 | `xuqm-go-sdk` |
各平台在实现时应在 API 签名和行为上保持一致;平台差异(如服务端 SDK 无 Push/IM 客户端能力)通过能力矩阵文档说明。
---
## 十一、待讨论 / 待决策事项
| 事项 | 当前状态 | 说明 |
|------|---------|------|
| userSig 过期自动刷新 | 待设计 | 是否由 SDK 内部定时刷新,还是 App 层感知过期后手动调用 `refreshToken`? |
| IM 未开通时 userSig 传入是否报警 | 待决策 | 建议在 debug 模式下打印警告 |
| PushSDK 厂商配置获取失败降级策略 | 待决策 | 获取厂商配置失败时是否还尝试使用上次缓存配置? |
| 配置文件格式版本向上兼容 | 进行中 | 见 `docs/配置文件规范.md` |
| 服务端 SDKJava/Go/Python的 setUserInfo 等效方法 | 待设计 | 服务端无设备/Push 概念,需要单独定义 |