diff --git a/docs-site/docs/.vitepress/config.ts b/docs-site/docs/.vitepress/config.ts index 70911f9..e461286 100644 --- a/docs-site/docs/.vitepress/config.ts +++ b/docs-site/docs/.vitepress/config.ts @@ -46,6 +46,7 @@ export default defineConfig({ { text: 'IM 接入', link: '/android/im' }, { text: '推送接入', link: '/android/push' }, { text: '版本管理', link: '/android/update' }, + { text: '授权管理', link: '/android/license' }, ], '/ios/': [ { text: '概览', link: '/ios/' }, diff --git a/docs-site/docs/android/license.md b/docs-site/docs/android/license.md new file mode 100644 index 0000000..3787f55 --- /dev/null +++ b/docs-site/docs/android/license.md @@ -0,0 +1,164 @@ +# Android 授权管理(License SDK) + +**模块**:`com.xuqm:sdk-license` · **最低 Android 版本**:API 24 (Android 7.0) + +License SDK 用于设备授权注册与验证,所有平台(Android / iOS / 鸿蒙)共用同一个 License Key。 + +--- + +## 快速接入 + +### 1. 添加依赖 + +```kotlin +// app/build.gradle.kts +dependencies { + implementation("com.xuqm:sdk-license:0.4.2") +} +``` + +### 2. 放置 License 文件 + +从租户平台下载 `.xuqmlicense` 加密文件,放入 app 的 assets 目录: + +``` +app/src/main/assets/xuqm/license.xuqm +``` + +SDK 会按以下顺序查找文件: +1. `assets/xuqm/license.xuqm` +2. `assets/xuqm/` 目录下第一个 `.xuqmlicense` 文件 + +### 3. 检查授权 + +SDK **无需手动初始化**,首次调用 `checkLicense()` 时自动读取并解密 License 文件。 + +```kotlin +LicenseSDK.checkLicense { isValid -> + if (isValid) { + // 授权通过,允许使用 + } else { + // 授权失败,提示用户 + } +} +``` + +--- + +## API 说明 + +### checkLicense + +检查设备授权状态。首次调用时自动完成:读取 License 文件 → 解密 → 注册/验证设备 → 缓存结果。 + +```kotlin +// 回调方式 +LicenseSDK.checkLicense { isValid -> + // true: 授权通过 false: 授权失败 +} + +// 协程方式 +lifecycleScope.launch { + val isValid = LicenseSDK.checkLicense() +} +``` + +**内部逻辑**: +1. 检查本地缓存(默认 10 分钟有效期) +2. 缓存有效 → 直接返回 `true` +3. 缓存过期或无缓存: + - 有 Token → 调用验证接口 + - 无 Token 或验证失败 → 调用注册接口 +4. 网络异常且缓存曾成功 → 返回 `true`(离线模式) +5. 网络异常且无缓存 → 返回 `false` + +--- + +## 设备唯一码 + +SDK 自动选择设备唯一码,优先级如下: + +| 优先级 | 来源 | 稳定性 | +|--------|------|--------| +| 1 | ANDROID_ID | 同一签名 + 设备 + 用户不变,重装不变 | +| 2 | 持久化 UUID | 首次生成后持久化存储 | + +**保持不变**的场景:App 重启、App 重装(同一签名)、系统升级 + +**会变化**的场景:恢复出厂设置、更换签名重新安装 + +--- + +## 离线模式 + +SDK 支持离线使用: +- 首次激活需要网络连接 +- 激活成功后,缓存有效期内(默认 10 分钟)可离线使用 +- 网络异常时,若缓存曾经成功验证,继续返回授权通过 + +--- + +## 数据存储 + +| 数据 | 存储方式 | 说明 | +|------|----------|------| +| deviceId | EncryptedSharedPreferences | AES-256 加密 | +| token | EncryptedSharedPreferences | AES-256 加密 | +| 授权状态 | EncryptedSharedPreferences | AES-256 加密 | +| fallback UUID | SharedPreferences | ANDROID_ID 不可用时使用 | + +--- + +## AppKey 变更 + +当 License 文件中的 AppKey 与已存储的不一致时,SDK 会自动: +1. 清除所有旧数据(Token、deviceId、状态) +2. 使用新 AppKey 重新注册 + +--- + +## 完整示例 + +```kotlin +class MyApplication : Application() { + override fun onCreate() { + super.onCreate() + // 无需初始化代码,SDK 自动读取 License 文件 + } +} + +class MainActivity : AppCompatActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + LicenseSDK.checkLicense { isValid -> + runOnUiThread { + if (isValid) { + // 进入主界面 + } else { + Toast.makeText(this, "授权验证失败", Toast.LENGTH_LONG).show() + finish() + } + } + } + } +} +``` + +--- + +## 常见问题 + +**Q: checkLicense 返回 false 怎么办?** +A: 检查以下几点: +- License 文件是否放在 `assets/xuqm/` 目录 +- 文件名是否为 `license.xuqm` 或以 `.xuqmlicense` 结尾 +- 设备是否有网络连接(首次激活需要网络) +- License 是否已过期或被管理员禁用 + +**Q: 不同平台可以用同一个 License 吗?** +A: 可以。所有平台共用同一个 AppKey,设备数量统一计算。 + +**Q: 设备被禁用后如何恢复?** +A: 由租户管理员在租户平台的授权管理页面重新激活该设备,App 端无需额外操作,下次 `checkLicense` 会自动恢复正常。 diff --git a/docs-site/docs/android/setup.md b/docs-site/docs/android/setup.md index 739c631..8629bc5 100644 --- a/docs-site/docs/android/setup.md +++ b/docs-site/docs/android/setup.md @@ -28,6 +28,7 @@ dependencyResolutionManagement { | sdk-im | `com.xuqm:sdk-im` | 单聊、群聊、消息收发、会话、好友、群组 | | sdk-push | `com.xuqm:sdk-push` | 自动检测厂商、设备 Token 注册(华为/小米/OPPO/vivo/荣耀/FCM)| | sdk-update | `com.xuqm:sdk-update` | App 更新检查、下载安装 | +| sdk-license | `com.xuqm:sdk-license` | 设备授权注册与验证 | 在 `app/build.gradle.kts` 中按需引入: @@ -37,6 +38,7 @@ dependencies { implementation("com.xuqm:sdk-im:0.4.2") implementation("com.xuqm:sdk-push:0.4.2") // 按需 implementation("com.xuqm:sdk-update:0.4.2") // 按需 + implementation("com.xuqm:sdk-license:0.4.2") // 按需 } ``` @@ -69,6 +71,9 @@ dependencies { # XuqmSDK Update -keep class com.xuqm.sdk.update.** { *; } +# XuqmSDK License +-keep class com.xuqm.sdk.license.** { *; } + # Gson(IM 消息序列化使用) -keepattributes Signature -keepattributes *Annotation* @@ -82,3 +87,4 @@ dependencies { - [Android IM 接入 →](./im) - [Android Push 接入 →](./push) - [Android 版本更新 →](./update) +- [Android 授权管理 →](./license) diff --git a/ops-platform/src/api/ops.ts b/ops-platform/src/api/ops.ts index 37e291d..0894315 100644 --- a/ops-platform/src/api/ops.ts +++ b/ops-platform/src/api/ops.ts @@ -76,6 +76,14 @@ export interface ServiceRequestPage { totalPages: number } +export interface LicenseStatusInfo { + exists: boolean + active?: boolean + maxDevices?: number + registeredDevices?: number + expiresAt?: string | null +} + export interface AppItem { id: string appKey: string @@ -227,8 +235,8 @@ export const opsApi = { listServiceRequests: (status = '', page = 0, size = 20) => client.get<{ data: ServiceRequestPage }>('/ops/service-requests', { params: { status, page, size } }), - approveRequest: (requestId: string, reviewNote = '') => - client.post<{ data: ServiceRequest }>(`/ops/service-requests/${requestId}/approve`, { reviewNote }), + approveRequest: (requestId: string, reviewNote = '', expiresAt?: string) => + client.post<{ data: ServiceRequest }>(`/ops/service-requests/${requestId}/approve`, { reviewNote, expiresAt }), rejectRequest: (requestId: string, reviewNote = '') => client.post<{ data: ServiceRequest }>(`/ops/service-requests/${requestId}/reject`, { reviewNote }), @@ -275,4 +283,10 @@ export const opsApi = { sendPushTestOffline: (payload: { appKey: string; userId: string; title: string; body: string; payload?: string }) => client.post<{ data: PushTestResult }>('/ops/push/test-offline', payload), + + getAppLicense: (appKey: string) => + client.get<{ data: LicenseStatusInfo }>(`/ops/apps/${appKey}/license`), + + updateMaxDevices: (appKey: string, maxDevices: number) => + client.put(`/ops/apps/${appKey}/license/max-devices`, { maxDevices }), } diff --git a/ops-platform/src/views/apps/AppDetailView.vue b/ops-platform/src/views/apps/AppDetailView.vue index 376f5ef..bd36350 100644 --- a/ops-platform/src/views/apps/AppDetailView.vue +++ b/ops-platform/src/views/apps/AppDetailView.vue @@ -31,7 +31,7 @@ - + @@ -49,20 +49,88 @@ + + + + + {{ licenseInfo.registeredDevices ?? '-' }} + + {{ licenseInfo.expiresAt ? fmt(licenseInfo.expiresAt) : '永久' }} + + +
+ {{ licenseInfo.maxDevices ?? '-' }} + + 修改 + +
+
+ + + 保存 + + 取消 +
+
+
+