diff --git a/docs/SDK设计规范.md b/docs/SDK设计规范.md new file mode 100644 index 0000000..43d35d8 --- /dev/null +++ b/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()` 一次调用,所有子 SDK(Push、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 无需调用任何初始化代码。 + +``` +配置文件路径(RN):src/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 +``` + +--- + +#### 已移除(不再提供) + +| 方法 | 移除原因 | +|------|---------| +| `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 // 等待初始化完成 +``` + +--- + +## 三、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 + +// 设置免打扰时间段(24小时制) +PushSDK.setQuietHours(start: string, end: string): Promise +// 示例:PushSDK.setQuietHours('22:00', '08:00') + +// 清除免打扰设置 +PushSDK.clearQuietHours(): Promise + +// 登出(通常不需要手动调用,setUserInfo(null) 会自动触发) +PushSDK.logout(): Promise +``` + +### 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 +``` + +或通过重新调用 `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 +``` + +#### Android APK 直接下载安装(新增) + +```ts +// 下载 APK 并在完成后调起系统安装器 +await UpdateSDK.downloadAndInstallApk( + downloadUrl: string, + options?: { + onProgress?: (progress: number) => void // 0~1 + sha256?: string // 校验值,有则验证 + } +): Promise +``` + +**完整整包更新流程示例**: + +```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 +``` + +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 +``` + +**SDK 内部自动完成**: +1. 调用 `checkPluginUpdate()` 确认有更新 +2. 下载 bundle(带进度回调) +3. 写入文件系统(`rn-bundles/..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`。 + +--- + +## 六、LicenseSDK(License 服务 — @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` | +| Vue3(H5) | `@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` | +| 服务端 SDK(Java/Go/Python)的 setUserInfo 等效方法 | 待设计 | 服务端无设备/Push 概念,需要单独定义 |