docs: fix version numbers, add license SDK docs; fix service gating reactivity
- Android: correct version header 0.5.x→0.4.x, add sdk-license to module table, update artifact versions to 0.4.10 - iOS: correct min version iOS 14→16, bump version to 0.2.0, update SPM ref to from: "0.2.0" - RN: fix version 0.3.x→0.2.x, standardize npm registry URL, add @xuqm/rn-license to module table - Flutter: update git ref to v0.2.2, add xuqm_flutter_license to module tables - Add new docs: ios/license, rn/push, rn/license, flutter/push, flutter/update, flutter/license - tenant-platform: make appKey a computed ref in Push/VersionManagementView to fix service gating reactivity on route change - tenant-platform: add requestActivation API endpoint - tenant-platform: add IM service gating UI (checkServiceEnabled + activation dialog) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
这个提交包含在:
父节点
65914b0ec2
当前提交
06436394ed
@ -54,13 +54,16 @@ export default defineConfig({
|
|||||||
{ text: 'IM 接入', link: '/ios/im' },
|
{ text: 'IM 接入', link: '/ios/im' },
|
||||||
{ text: '推送接入', link: '/ios/push' },
|
{ text: '推送接入', link: '/ios/push' },
|
||||||
{ text: '版本管理', link: '/ios/update' },
|
{ text: '版本管理', link: '/ios/update' },
|
||||||
|
{ text: '授权管理', link: '/ios/license' },
|
||||||
],
|
],
|
||||||
'/rn/': [
|
'/rn/': [
|
||||||
{ text: '概览', link: '/rn/' },
|
{ text: '概览', link: '/rn/' },
|
||||||
{ text: '安装配置', link: '/rn/setup' },
|
{ text: '安装配置', link: '/rn/setup' },
|
||||||
{ text: 'IM 接入', link: '/rn/im' },
|
{ text: 'IM 接入', link: '/rn/im' },
|
||||||
{ text: '群聊', link: '/rn/group' },
|
{ text: '群聊', link: '/rn/group' },
|
||||||
|
{ text: '推送接入', link: '/rn/push' },
|
||||||
{ text: '版本管理', link: '/rn/update' },
|
{ text: '版本管理', link: '/rn/update' },
|
||||||
|
{ text: '授权管理', link: '/rn/license' },
|
||||||
],
|
],
|
||||||
'/vue3/': [
|
'/vue3/': [
|
||||||
{ text: '概览', link: '/vue3/' },
|
{ text: '概览', link: '/vue3/' },
|
||||||
@ -71,6 +74,9 @@ export default defineConfig({
|
|||||||
{ text: '概览', link: '/flutter/' },
|
{ text: '概览', link: '/flutter/' },
|
||||||
{ text: '安装配置', link: '/flutter/setup' },
|
{ text: '安装配置', link: '/flutter/setup' },
|
||||||
{ text: 'IM 接入', link: '/flutter/im' },
|
{ text: 'IM 接入', link: '/flutter/im' },
|
||||||
|
{ text: '推送接入', link: '/flutter/push' },
|
||||||
|
{ text: '版本管理', link: '/flutter/update' },
|
||||||
|
{ text: '授权管理', link: '/flutter/license' },
|
||||||
],
|
],
|
||||||
'/harmony/': [
|
'/harmony/': [
|
||||||
{ text: '概览', link: '/harmony/' },
|
{ text: '概览', link: '/harmony/' },
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# Android SDK 接入指南
|
# Android SDK 接入指南
|
||||||
|
|
||||||
**版本**:0.5.x(UserSig 鉴权 · 简化登录)· **最低 Android 版本**:API 24 (Android 7.0) · **语言**:Kotlin
|
**版本**:0.4.x · **最低 Android 版本**:API 24 (Android 7.0) · **语言**:Kotlin
|
||||||
|
|
||||||
## 功能模块
|
## 功能模块
|
||||||
|
|
||||||
@ -10,6 +10,7 @@
|
|||||||
| sdk-im | `com.xuqm:sdk-im` | 单聊、群聊、消息收发、会话、好友、群组 |
|
| sdk-im | `com.xuqm:sdk-im` | 单聊、群聊、消息收发、会话、好友、群组 |
|
||||||
| sdk-push | `com.xuqm:sdk-push` | 自动检测厂商、设备 Token 注册(华为/小米/OPPO/vivo/荣耀/FCM) |
|
| sdk-push | `com.xuqm:sdk-push` | 自动检测厂商、设备 Token 注册(华为/小米/OPPO/vivo/荣耀/FCM) |
|
||||||
| sdk-update | `com.xuqm:sdk-update` | App 更新检查、下载安装 |
|
| sdk-update | `com.xuqm:sdk-update` | App 更新检查、下载安装 |
|
||||||
|
| sdk-license | `com.xuqm:sdk-license` | 设备授权注册与验证 |
|
||||||
|
|
||||||
## 快速接入
|
## 快速接入
|
||||||
|
|
||||||
@ -28,10 +29,11 @@ dependencyResolutionManagement {
|
|||||||
```kotlin
|
```kotlin
|
||||||
// app/build.gradle.kts
|
// app/build.gradle.kts
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("com.xuqm:sdk-core:0.4.2")
|
implementation("com.xuqm:sdk-core:0.4.10")
|
||||||
implementation("com.xuqm:sdk-im:0.4.2")
|
implementation("com.xuqm:sdk-im:0.4.10")
|
||||||
implementation("com.xuqm:sdk-push:0.4.2") // 按需
|
implementation("com.xuqm:sdk-push:0.4.10") // 按需
|
||||||
implementation("com.xuqm:sdk-update:0.4.2") // 按需
|
implementation("com.xuqm:sdk-update:0.4.10") // 按需
|
||||||
|
implementation("com.xuqm:sdk-license:0.4.10") // 按需
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -188,3 +190,13 @@ lifecycleScope.launch {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 下一步
|
||||||
|
|
||||||
|
- [Android 安装配置 →](./setup)
|
||||||
|
- [Android IM 接入 →](./im)
|
||||||
|
- [Android 推送接入 →](./push)
|
||||||
|
- [Android 版本更新 →](./update)
|
||||||
|
- [Android 授权管理 →](./license)
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# Android 安装配置
|
# Android 安装配置
|
||||||
|
|
||||||
**版本**:0.5.x · **最低 Android 版本**:API 24 (Android 7.0) · **语言**:Kotlin
|
**版本**:0.4.x · **最低 Android 版本**:API 24 (Android 7.0) · **语言**:Kotlin
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -34,11 +34,11 @@ dependencyResolutionManagement {
|
|||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
dependencies {
|
dependencies {
|
||||||
implementation("com.xuqm:sdk-core:0.4.2")
|
implementation("com.xuqm:sdk-core:0.4.10")
|
||||||
implementation("com.xuqm:sdk-im:0.4.2")
|
implementation("com.xuqm:sdk-im:0.4.10")
|
||||||
implementation("com.xuqm:sdk-push:0.4.2") // 按需
|
implementation("com.xuqm:sdk-push:0.4.10") // 按需
|
||||||
implementation("com.xuqm:sdk-update:0.4.2") // 按需
|
implementation("com.xuqm:sdk-update:0.4.10") // 按需
|
||||||
implementation("com.xuqm:sdk-license:0.4.2") // 按需
|
implementation("com.xuqm:sdk-license:0.4.10") // 按需
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@ -14,6 +14,7 @@
|
|||||||
| `xuqm_flutter_im` | `packages/im` | 单聊、群聊、消息收发、会话、好友、群组 |
|
| `xuqm_flutter_im` | `packages/im` | 单聊、群聊、消息收发、会话、好友、群组 |
|
||||||
| `xuqm_flutter_push` | `packages/push` | 设备 Token 注册、厂商检测(Android)/ APNs(iOS)|
|
| `xuqm_flutter_push` | `packages/push` | 设备 Token 注册、厂商检测(Android)/ APNs(iOS)|
|
||||||
| `xuqm_flutter_update` | `packages/update` | App 版本检查、商店跳转、APK 下载(Android)|
|
| `xuqm_flutter_update` | `packages/update` | App 版本检查、商店跳转、APK 下载(Android)|
|
||||||
|
| `xuqm_flutter_license` | `packages/license` | 设备授权注册与验证(License SDK)|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -26,7 +27,7 @@ dependencies:
|
|||||||
xuqm_flutter_sdk:
|
xuqm_flutter_sdk:
|
||||||
git:
|
git:
|
||||||
url: https://xuqinmin.com/xuqinmin12/XuqmGroup-FlutterSDK.git
|
url: https://xuqinmin.com/xuqinmin12/XuqmGroup-FlutterSDK.git
|
||||||
ref: v0.2.0
|
ref: v0.2.2
|
||||||
```
|
```
|
||||||
|
|
||||||
> Gitea Package Registry 暂不支持 Dart/Flutter 包格式,因此通过 Git Tag 方式发布。
|
> Gitea Package Registry 暂不支持 Dart/Flutter 包格式,因此通过 Git Tag 方式发布。
|
||||||
@ -48,11 +49,6 @@ await XuqmSDK.initialize(XuqmInitOptions(
|
|||||||
|
|
||||||
初始化时会自动向服务端请求远程配置(IM API 地址等),若网络异常则回退到内置默认值。
|
初始化时会自动向服务端请求远程配置(IM API 地址等),若网络异常则回退到内置默认值。
|
||||||
|
|
||||||
|
|
||||||
```dart
|
|
||||||
XuqmSDK.init(XuqmInitOptions(appKey: 'your_app_key'));
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### 2. IM 登录
|
### 2. IM 登录
|
||||||
|
|||||||
205
docs-site/docs/flutter/license.md
普通文件
205
docs-site/docs/flutter/license.md
普通文件
@ -0,0 +1,205 @@
|
|||||||
|
# Flutter 授权管理(License SDK)
|
||||||
|
|
||||||
|
**模块**:`xuqm_flutter_license` · **依赖**:`cryptography`、`device_info_plus`、`shared_preferences`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 安装
|
||||||
|
|
||||||
|
在 `pubspec.yaml` 中添加:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
dependencies:
|
||||||
|
xuqm_flutter_license:
|
||||||
|
git:
|
||||||
|
url: https://xuqinmin.com/xuqinmin12/XuqmGroup-FlutterSDK.git
|
||||||
|
ref: v0.2.2
|
||||||
|
path: packages/license
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 放置 License 文件
|
||||||
|
|
||||||
|
从租户平台下载 `.xuqmlicense` 加密文件,放入 Flutter assets:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
# pubspec.yaml
|
||||||
|
flutter:
|
||||||
|
assets:
|
||||||
|
- assets/xuqm/license.xuqm
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 检查授权
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:flutter/services.dart' show rootBundle;
|
||||||
|
import 'package:xuqm_flutter_license/xuqm_license.dart';
|
||||||
|
|
||||||
|
Future<void> verifyLicense() async {
|
||||||
|
// 从 assets 读取加密文件
|
||||||
|
final content = await rootBundle.loadString('assets/xuqm/license.xuqm');
|
||||||
|
|
||||||
|
// 自动解密并初始化
|
||||||
|
await initializeFromFile(content);
|
||||||
|
|
||||||
|
// 检查授权(有缓存时直接返回,否则网络验证)
|
||||||
|
final result = await checkLicense();
|
||||||
|
switch (result) {
|
||||||
|
case LicenseSuccess(reason: final r):
|
||||||
|
print('授权通过: $r');
|
||||||
|
case LicenseError(message: final m):
|
||||||
|
print('授权失败: $m');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 携带用户信息
|
||||||
|
|
||||||
|
```dart
|
||||||
|
final result = await checkLicense(
|
||||||
|
userInfo: LicenseUserInfo(
|
||||||
|
userId: 'user_001',
|
||||||
|
name: '张三',
|
||||||
|
email: 'zhangsan@company.com',
|
||||||
|
),
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. API 说明
|
||||||
|
|
||||||
|
### initialize
|
||||||
|
|
||||||
|
```dart
|
||||||
|
void initialize(String appKey, {String? baseUrl, String? deviceName})
|
||||||
|
```
|
||||||
|
|
||||||
|
手动初始化,适用于不使用 License 文件的场景。
|
||||||
|
|
||||||
|
### initializeFromFile
|
||||||
|
|
||||||
|
```dart
|
||||||
|
Future<void> initializeFromFile(String encryptedContent)
|
||||||
|
```
|
||||||
|
|
||||||
|
从加密 License 文件内容自动解析 appKey 和 baseUrl 并初始化。
|
||||||
|
|
||||||
|
### checkLicense
|
||||||
|
|
||||||
|
```dart
|
||||||
|
Future<LicenseResult> checkLicense({LicenseUserInfo? userInfo})
|
||||||
|
```
|
||||||
|
|
||||||
|
返回 `LicenseSuccess(reason)` 或 `LicenseError(message)`(密封类,使用 `switch` 匹配)。
|
||||||
|
|
||||||
|
**缓存策略**:10 分钟有效期,有效期内不发起网络请求。
|
||||||
|
|
||||||
|
### getStatus
|
||||||
|
|
||||||
|
```dart
|
||||||
|
Future<LicenseStatus> getStatus() // LicenseStatus.ok / .denied / .unknown
|
||||||
|
```
|
||||||
|
|
||||||
|
### getDeviceId
|
||||||
|
|
||||||
|
```dart
|
||||||
|
Future<String?> getDeviceId()
|
||||||
|
```
|
||||||
|
|
||||||
|
### clear
|
||||||
|
|
||||||
|
```dart
|
||||||
|
Future<void> clear()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 设备唯一码
|
||||||
|
|
||||||
|
| 平台 | 来源 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| Android | `androidInfo.id` | `Settings.Secure.ANDROID_ID` |
|
||||||
|
| iOS | `iosInfo.identifierForVendor` | 同 Vendor 下卸载重装不变 |
|
||||||
|
| 其他 | 生成 UUID 存入 SharedPreferences | fallback |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 数据存储
|
||||||
|
|
||||||
|
| 数据 | 存储方式 | 说明 |
|
||||||
|
|------|----------|------|
|
||||||
|
| deviceId | SharedPreferences | 持久化 |
|
||||||
|
| token | SharedPreferences | 持久化 |
|
||||||
|
| 授权状态 | SharedPreferences | 非敏感,持久化 |
|
||||||
|
| statusTime | SharedPreferences | 缓存有效期判断 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 离线模式
|
||||||
|
|
||||||
|
- 首次激活需要网络连接
|
||||||
|
- 激活后 10 分钟缓存内可离线使用
|
||||||
|
- 网络异常时,若历史缓存成功,继续返回 `LicenseSuccess`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 完整示例
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
import 'package:xuqm_flutter_sdk/xuqm_flutter_sdk.dart';
|
||||||
|
import 'package:xuqm_flutter_license/xuqm_license.dart' as license;
|
||||||
|
|
||||||
|
void main() async {
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
await XuqmSDK.initialize(XuqmInitOptions(appKey: 'your_app_key'));
|
||||||
|
|
||||||
|
// 初始化 License
|
||||||
|
final content = await rootBundle.loadString('assets/xuqm/license.xuqm');
|
||||||
|
await license.initializeFromFile(content);
|
||||||
|
|
||||||
|
runApp(const MyApp());
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyApp extends StatelessWidget {
|
||||||
|
const MyApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return MaterialApp(
|
||||||
|
home: FutureBuilder(
|
||||||
|
future: license.checkLicense(
|
||||||
|
userInfo: license.LicenseUserInfo(userId: 'user_001'),
|
||||||
|
),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (!snapshot.hasData) return const CircularProgressIndicator();
|
||||||
|
final result = snapshot.data!;
|
||||||
|
if (result is license.LicenseError) {
|
||||||
|
return Scaffold(
|
||||||
|
body: Center(child: Text('授权失败: ${result.message}')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return const HomePage();
|
||||||
|
},
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. 常见问题
|
||||||
|
|
||||||
|
**Q: 提示 `Invalid license file format`?**
|
||||||
|
检查 License 文件是否完整,以及是否已在 `pubspec.yaml` 的 `flutter.assets` 中声明。
|
||||||
|
|
||||||
|
**Q: 不同平台可以用同一个 License 吗?**
|
||||||
|
可以,所有平台共用同一个 AppKey,设备数量统一计算。
|
||||||
144
docs-site/docs/flutter/push.md
普通文件
144
docs-site/docs/flutter/push.md
普通文件
@ -0,0 +1,144 @@
|
|||||||
|
# Flutter 推送接入指南
|
||||||
|
|
||||||
|
**模块**:`xuqm_flutter_push` · **支持**:华为、小米、OPPO、vivo、荣耀(Android)、APNs(iOS)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 安装
|
||||||
|
|
||||||
|
在 `pubspec.yaml` 中添加:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
dependencies:
|
||||||
|
xuqm_flutter_push:
|
||||||
|
git:
|
||||||
|
url: https://xuqinmin.com/xuqinmin12/XuqmGroup-FlutterSDK.git
|
||||||
|
ref: v0.2.0
|
||||||
|
path: packages/push
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Android 厂商推送集成
|
||||||
|
|
||||||
|
在 `android/app/build.gradle` 中按需添加厂商 SDK:
|
||||||
|
|
||||||
|
```gradle
|
||||||
|
dependencies {
|
||||||
|
// 华为 HMS Push
|
||||||
|
implementation 'com.huawei.hms:push:6.9.0.300'
|
||||||
|
// 小米 Push(其他厂商同理)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 请求原生推送注册
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:xuqm_flutter_push/xuqm_flutter_push.dart';
|
||||||
|
|
||||||
|
// 触发原生推送注册(Android: 厂商 token;iOS: APNs 权限)
|
||||||
|
await XuqmPushSdk.requestNativeRegistration();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 监听推送 Token
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:xuqm_flutter_push/xuqm_flutter_push.dart';
|
||||||
|
|
||||||
|
// 监听 token 回调(广播 Stream,可多处监听)
|
||||||
|
final subscription = XuqmPushSdk.onPushToken.listen((event) async {
|
||||||
|
final token = event['token'] ?? '';
|
||||||
|
final vendor = event['vendor'] ?? '';
|
||||||
|
print('获取到 Token: $token, 厂商: $vendor');
|
||||||
|
|
||||||
|
// 登录后注册到服务端
|
||||||
|
final push = XuqmPushSdk();
|
||||||
|
await push.registerToken(
|
||||||
|
'user_001',
|
||||||
|
token,
|
||||||
|
vendor: vendor,
|
||||||
|
platform: Platform.isIOS ? 'IOS' : 'ANDROID',
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 页面销毁时取消监听
|
||||||
|
subscription.cancel();
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 手动注册 Token
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:xuqm_flutter_push/xuqm_flutter_push.dart';
|
||||||
|
import 'dart:io';
|
||||||
|
|
||||||
|
final push = XuqmPushSdk();
|
||||||
|
|
||||||
|
await push.registerToken(
|
||||||
|
'user_001', // userId
|
||||||
|
'device_token', // token
|
||||||
|
vendor: 'HUAWEI',
|
||||||
|
platform: Platform.isIOS ? 'IOS' : 'ANDROID',
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 登出时注销 Token
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:xuqm_flutter_common/xuqm_flutter_common.dart' as common;
|
||||||
|
|
||||||
|
final push = XuqmPushSdk();
|
||||||
|
final deviceId = await common.apiRequest('/api/device/id');
|
||||||
|
|
||||||
|
await push.unregisterToken('user_001', deviceId: deviceId);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 检测厂商
|
||||||
|
|
||||||
|
```dart
|
||||||
|
final vendor = await XuqmPushSdk.detectVendor();
|
||||||
|
print('当前推送厂商: $vendor'); // e.g. 'HUAWEI', 'XIAOMI', 'APNS'
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 多模块统一登录
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:xuqm_flutter_sdk/xuqm_flutter_sdk.dart';
|
||||||
|
import 'package:xuqm_flutter_push/xuqm_flutter_push.dart';
|
||||||
|
|
||||||
|
// 初始化 SDK
|
||||||
|
await XuqmSDK.initialize(XuqmInitOptions(appKey: 'your_app_key'));
|
||||||
|
|
||||||
|
// 请求原生注册,监听 token 后手动上报
|
||||||
|
await XuqmPushSdk.requestNativeRegistration();
|
||||||
|
XuqmPushSdk.onPushToken.listen((event) async {
|
||||||
|
final push = XuqmPushSdk();
|
||||||
|
await push.registerToken('user_001', event['token']!);
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. iOS APNs 配置
|
||||||
|
|
||||||
|
在 Xcode 中开启 Push Notifications 能力(Signing & Capabilities → + Capability → Push Notifications)。
|
||||||
|
|
||||||
|
`Info.plist` 添加(如需后台推送):
|
||||||
|
|
||||||
|
```xml
|
||||||
|
<key>UIBackgroundModes</key>
|
||||||
|
<array>
|
||||||
|
<string>remote-notification</string>
|
||||||
|
</array>
|
||||||
|
```
|
||||||
@ -13,7 +13,7 @@ dependencies:
|
|||||||
xuqm_flutter_sdk:
|
xuqm_flutter_sdk:
|
||||||
git:
|
git:
|
||||||
url: https://xuqinmin.com/xuqinmin12/XuqmGroup-FlutterSDK.git
|
url: https://xuqinmin.com/xuqinmin12/XuqmGroup-FlutterSDK.git
|
||||||
ref: v0.2.0
|
ref: v0.2.2
|
||||||
```
|
```
|
||||||
|
|
||||||
> Gitea Package Registry 暂不支持 Dart/Flutter 包格式,因此通过 Git Tag 方式发布。
|
> Gitea Package Registry 暂不支持 Dart/Flutter 包格式,因此通过 Git Tag 方式发布。
|
||||||
@ -28,6 +28,7 @@ dependencies:
|
|||||||
| `xuqm_flutter_im` | `packages/im` | 单聊、群聊、消息收发、会话、好友、群组 |
|
| `xuqm_flutter_im` | `packages/im` | 单聊、群聊、消息收发、会话、好友、群组 |
|
||||||
| `xuqm_flutter_push` | `packages/push` | 设备 Token 注册、厂商检测(Android)/ APNs(iOS)|
|
| `xuqm_flutter_push` | `packages/push` | 设备 Token 注册、厂商检测(Android)/ APNs(iOS)|
|
||||||
| `xuqm_flutter_update` | `packages/update` | App 版本检查、商店跳转、APK 下载(Android)|
|
| `xuqm_flutter_update` | `packages/update` | App 版本检查、商店跳转、APK 下载(Android)|
|
||||||
|
| `xuqm_flutter_license` | `packages/license` | 设备授权注册与验证(License SDK)|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -66,3 +67,6 @@ await XuqmImSdk().logout();
|
|||||||
## 下一步
|
## 下一步
|
||||||
|
|
||||||
- [Flutter IM 接入 →](./im)
|
- [Flutter IM 接入 →](./im)
|
||||||
|
- [Flutter 推送接入 →](./push)
|
||||||
|
- [Flutter 版本更新 →](./update)
|
||||||
|
- [Flutter 授权管理 →](./license)
|
||||||
|
|||||||
180
docs-site/docs/flutter/update.md
普通文件
180
docs-site/docs/flutter/update.md
普通文件
@ -0,0 +1,180 @@
|
|||||||
|
# Flutter 版本更新接入指南
|
||||||
|
|
||||||
|
**模块**:`xuqm_flutter_update` · **功能**:App 版本检查、商店跳转、APK 直接下载
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 安装
|
||||||
|
|
||||||
|
在 `pubspec.yaml` 中添加:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
dependencies:
|
||||||
|
xuqm_flutter_update:
|
||||||
|
git:
|
||||||
|
url: https://xuqinmin.com/xuqinmin12/XuqmGroup-FlutterSDK.git
|
||||||
|
ref: v0.2.0
|
||||||
|
path: packages/update
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 检查版本更新
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:xuqm_flutter_update/xuqm_flutter_update.dart';
|
||||||
|
|
||||||
|
final sdk = XuqmUpdateSdk();
|
||||||
|
final info = await sdk.checkAppUpdate();
|
||||||
|
|
||||||
|
if (info.needsUpdate) {
|
||||||
|
print('新版本: ${info.versionName}');
|
||||||
|
print('更新说明: ${info.changeLog}');
|
||||||
|
print('强制更新: ${info.forceUpdate}');
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
`checkAppUpdate()` 会自动读取当前 App 的 versionCode 和 versionName(通过 `package_info_plus`),并与服务端对比。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 打开商店
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// 自动判断平台:iOS → App Store,Android → marketUrl
|
||||||
|
await sdk.openStore(info);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 直接下载 APK(Android)
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// 打开 APK 下载链接(使用系统浏览器或下载器)
|
||||||
|
await sdk.openDownloadUrl(info);
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 强制更新处理
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:xuqm_flutter_update/xuqm_flutter_update.dart';
|
||||||
|
|
||||||
|
Future<void> checkAndHandleUpdate(BuildContext context) async {
|
||||||
|
final sdk = XuqmUpdateSdk();
|
||||||
|
final info = await sdk.checkAppUpdate();
|
||||||
|
|
||||||
|
if (!info.needsUpdate) return;
|
||||||
|
|
||||||
|
if (info.forceUpdate) {
|
||||||
|
// 强制更新:不可关闭的 Dialog
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: false,
|
||||||
|
builder: (_) => AlertDialog(
|
||||||
|
title: const Text('发现重要更新'),
|
||||||
|
content: Text('当前版本已不可用,请升级至 ${info.versionName}'),
|
||||||
|
actions: [
|
||||||
|
TextButton(
|
||||||
|
onPressed: () => sdk.openStore(info),
|
||||||
|
child: const Text('立即更新'),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// 可选更新:普通 Dialog
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
builder: (_) => AlertDialog(
|
||||||
|
title: Text('发现新版本 ${info.versionName}'),
|
||||||
|
content: Text(info.changeLog ?? ''),
|
||||||
|
actions: [
|
||||||
|
TextButton(onPressed: () => Navigator.pop(context), child: const Text('稍后')),
|
||||||
|
TextButton(onPressed: () => sdk.openStore(info), child: const Text('立即更新')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. AppUpdateInfo 字段说明
|
||||||
|
|
||||||
|
| 字段 | 类型 | 说明 |
|
||||||
|
|------|------|------|
|
||||||
|
| `needsUpdate` | `bool` | 是否有新版本 |
|
||||||
|
| `forceUpdate` | `bool` | 是否强制更新 |
|
||||||
|
| `versionName` | `String?` | 新版本名称(如 `1.2.3`)|
|
||||||
|
| `versionCode` | `int?` | 新版本号 |
|
||||||
|
| `changeLog` | `String?` | 更新说明 |
|
||||||
|
| `downloadUrl` | `String?` | APK 直接下载地址(Android)|
|
||||||
|
| `appStoreUrl` | `String?` | App Store 链接(iOS)|
|
||||||
|
| `marketUrl` | `String?` | 各大应用商店链接(Android)|
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 开发模式(模拟低版本)
|
||||||
|
|
||||||
|
```dart
|
||||||
|
// 仅在开发/测试时使用,模拟当前版本为 1,触发更新提示
|
||||||
|
XuqmUpdateSdk.devSetAppVersion(1, '0.0.1');
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 完整示例
|
||||||
|
|
||||||
|
```dart
|
||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:xuqm_flutter_sdk/xuqm_flutter_sdk.dart';
|
||||||
|
import 'package:xuqm_flutter_update/xuqm_flutter_update.dart';
|
||||||
|
|
||||||
|
void main() async {
|
||||||
|
WidgetsFlutterBinding.ensureInitialized();
|
||||||
|
await XuqmSDK.initialize(XuqmInitOptions(appKey: 'your_app_key'));
|
||||||
|
runApp(const MyApp());
|
||||||
|
}
|
||||||
|
|
||||||
|
class MyApp extends StatefulWidget {
|
||||||
|
const MyApp({super.key});
|
||||||
|
|
||||||
|
@override
|
||||||
|
State<MyApp> createState() => _MyAppState();
|
||||||
|
}
|
||||||
|
|
||||||
|
class _MyAppState extends State<MyApp> {
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
WidgetsBinding.instance.addPostFrameCallback((_) => _checkUpdate());
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _checkUpdate() async {
|
||||||
|
final sdk = XuqmUpdateSdk();
|
||||||
|
final info = await sdk.checkAppUpdate();
|
||||||
|
if (info.needsUpdate && mounted) {
|
||||||
|
showDialog(
|
||||||
|
context: context,
|
||||||
|
barrierDismissible: !info.forceUpdate,
|
||||||
|
builder: (_) => AlertDialog(
|
||||||
|
title: Text('新版本 ${info.versionName}'),
|
||||||
|
content: Text(info.changeLog ?? ''),
|
||||||
|
actions: [
|
||||||
|
if (!info.forceUpdate)
|
||||||
|
TextButton(onPressed: () => Navigator.pop(context), child: const Text('稍后')),
|
||||||
|
TextButton(onPressed: () => sdk.openStore(info), child: const Text('更新')),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) => const MaterialApp(home: Scaffold());
|
||||||
|
}
|
||||||
|
```
|
||||||
@ -1,6 +1,6 @@
|
|||||||
# iOS SDK 概览
|
# iOS SDK 概览
|
||||||
|
|
||||||
**版本**:0.1.0 · **最低 iOS 版本**:iOS 14 · **语言**:Swift 5.9+
|
**版本**:0.2.0 · **最低 iOS 版本**:iOS 16 · **语言**:Swift 5.9+
|
||||||
|
|
||||||
## 功能模块
|
## 功能模块
|
||||||
|
|
||||||
|
|||||||
190
docs-site/docs/ios/license.md
普通文件
190
docs-site/docs/ios/license.md
普通文件
@ -0,0 +1,190 @@
|
|||||||
|
# iOS 授权管理(License SDK)
|
||||||
|
|
||||||
|
**模块**:`LicenseSDK`(包含在 `XuqmSDK` 中)· **最低 iOS 版本**:iOS 16
|
||||||
|
|
||||||
|
License SDK 用于设备授权注册与验证,Android / iOS / RN / Flutter 共用同一个 AppKey,设备数量统一计算。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 快速接入
|
||||||
|
|
||||||
|
### 1. 添加依赖
|
||||||
|
|
||||||
|
License 模块已内置于 `XuqmSDK`,无需额外安装包:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
// Package.swift 或 Xcode → File → Add Package Dependencies
|
||||||
|
// URL: https://xuqinmin.com/xuqinmin12/XuqmGroup-iOSSDK
|
||||||
|
import XuqmSDK
|
||||||
|
```
|
||||||
|
|
||||||
|
### 2. 放置 License 文件
|
||||||
|
|
||||||
|
从租户平台下载 `.xuqmlicense` 加密文件,放入 App Bundle:
|
||||||
|
|
||||||
|
```
|
||||||
|
MyApp/Resources/xuqm/license.xuqm
|
||||||
|
```
|
||||||
|
|
||||||
|
在 Xcode 中将该文件添加到 Target → Build Phases → Copy Bundle Resources,确保它出现在 App Bundle 中。SDK 会自动读取 `Bundle.main` 中的 `xuqm/license.xuqm`。
|
||||||
|
|
||||||
|
### 3. 检查授权
|
||||||
|
|
||||||
|
SDK **无需手动初始化**,首次调用 `checkLicense()` 时自动读取并解密 License 文件:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
import XuqmSDK
|
||||||
|
|
||||||
|
Task {
|
||||||
|
let result = await LicenseSDK.shared.checkLicense()
|
||||||
|
switch result {
|
||||||
|
case .success(let reason):
|
||||||
|
print("授权通过:\(reason)")
|
||||||
|
case .error(let message):
|
||||||
|
print("授权失败:\(message)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## API 说明
|
||||||
|
|
||||||
|
### checkLicense
|
||||||
|
|
||||||
|
```swift
|
||||||
|
func checkLicense(userInfo: LicenseUserInfo? = nil) async -> LicenseResult
|
||||||
|
```
|
||||||
|
|
||||||
|
**内部逻辑**:
|
||||||
|
1. 检查本地缓存(默认 10 分钟有效期)
|
||||||
|
2. 缓存有效 → 直接返回 `.success`
|
||||||
|
3. 缓存过期或无缓存:
|
||||||
|
- 有 Token → 调用验证接口
|
||||||
|
- 无 Token 或验证失败 → 调用注册接口
|
||||||
|
4. 网络异常且缓存曾成功 → 返回 `.success("Offline - cached ok")`
|
||||||
|
5. 网络异常且无缓存 → 返回 `.error`
|
||||||
|
|
||||||
|
### getStatus
|
||||||
|
|
||||||
|
```swift
|
||||||
|
func getStatus() -> LicenseStatus // .ok / .denied / .unknown
|
||||||
|
```
|
||||||
|
|
||||||
|
同步查询当前状态(不发起网络请求)。
|
||||||
|
|
||||||
|
### getDeviceId
|
||||||
|
|
||||||
|
```swift
|
||||||
|
func getDeviceId() -> String?
|
||||||
|
```
|
||||||
|
|
||||||
|
### clear
|
||||||
|
|
||||||
|
```swift
|
||||||
|
func clear()
|
||||||
|
```
|
||||||
|
|
||||||
|
清除所有本地授权数据(token、deviceId、状态)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 携带用户信息
|
||||||
|
|
||||||
|
注册时可携带业务用户信息,方便在租户平台进行设备管理:
|
||||||
|
|
||||||
|
```swift
|
||||||
|
let userInfo = LicenseUserInfo(
|
||||||
|
userId: "user_001",
|
||||||
|
name: "张三",
|
||||||
|
email: "zhangsan@company.com"
|
||||||
|
)
|
||||||
|
|
||||||
|
let result = await LicenseSDK.shared.checkLicense(userInfo: userInfo)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 设备唯一码
|
||||||
|
|
||||||
|
| 优先级 | 来源 | 说明 |
|
||||||
|
|--------|------|------|
|
||||||
|
| 1 | `UIDevice.identifierForVendor` | 同一 App 卸载重装不变(同 Vendor)|
|
||||||
|
| 2 | 首次生成 UUID 存入 Keychain | identifierForVendor 不可用时 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 数据存储
|
||||||
|
|
||||||
|
| 数据 | 存储方式 | 说明 |
|
||||||
|
|------|----------|------|
|
||||||
|
| deviceId | Keychain(Security framework)| 加密持久化 |
|
||||||
|
| token | Keychain | 加密持久化 |
|
||||||
|
| 授权状态 | UserDefaults(Suite: `xuqm_license`)| 非敏感 |
|
||||||
|
| statusTime | UserDefaults | 缓存有效期判断 |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 离线模式
|
||||||
|
|
||||||
|
- 首次激活需要网络连接
|
||||||
|
- 激活成功后,10 分钟缓存内可离线使用
|
||||||
|
- 网络异常时,若历史缓存成功,继续返回授权通过
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 手动初始化(高级用法)
|
||||||
|
|
||||||
|
```swift
|
||||||
|
// 不使用 License 文件,手动指定 AppKey
|
||||||
|
LicenseSDK.shared.initialize(
|
||||||
|
appKey: "your_app_key",
|
||||||
|
baseUrl: "https://auth.dev.xuqinmin.com",
|
||||||
|
deviceName: "我的 iPhone"
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 完整示例
|
||||||
|
|
||||||
|
```swift
|
||||||
|
import XuqmSDK
|
||||||
|
|
||||||
|
@main
|
||||||
|
struct MyApp: App {
|
||||||
|
var body: some Scene {
|
||||||
|
WindowGroup {
|
||||||
|
ContentView()
|
||||||
|
.task { await checkLicense() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkLicense() async {
|
||||||
|
let result = await LicenseSDK.shared.checkLicense(
|
||||||
|
userInfo: LicenseUserInfo(userId: "user_001")
|
||||||
|
)
|
||||||
|
switch result {
|
||||||
|
case .success:
|
||||||
|
break // 授权通过,正常使用
|
||||||
|
case .error(let msg):
|
||||||
|
// 提示用户,限制功能入口
|
||||||
|
print("License 验证失败: \(msg)")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 常见问题
|
||||||
|
|
||||||
|
**Q: checkLicense 返回 error 怎么办?**
|
||||||
|
检查:
|
||||||
|
- License 文件是否已添加到 Build Phases → Copy Bundle Resources
|
||||||
|
- 文件路径是否为 `xuqm/license.xuqm`(大小写敏感)
|
||||||
|
- 设备是否有网络连接(首次激活需要网络)
|
||||||
|
- License 是否已过期或被管理员禁用
|
||||||
|
|
||||||
|
**Q: 不同平台可以用同一个 License 吗?**
|
||||||
|
可以,所有平台共用同一个 AppKey,设备数量统一计算。
|
||||||
@ -1,6 +1,6 @@
|
|||||||
# iOS 安装配置
|
# iOS 安装配置
|
||||||
|
|
||||||
**版本**:0.1.0 · **最低 iOS 版本**:iOS 16 · **语言**:Swift 5.9+
|
**版本**:0.2.0 · **最低 iOS 版本**:iOS 16 · **语言**:Swift 5.9+
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -22,7 +22,7 @@ let package = Package(
|
|||||||
name: "MyApp",
|
name: "MyApp",
|
||||||
platforms: [.iOS(.v16)],
|
platforms: [.iOS(.v16)],
|
||||||
dependencies: [
|
dependencies: [
|
||||||
.package(url: "https://xuqinmin.com/xuqinmin12/XuqmGroup-iOSSDK", from: "0.1.0")
|
.package(url: "https://xuqinmin.com/xuqinmin12/XuqmGroup-iOSSDK", from: "0.2.0")
|
||||||
],
|
],
|
||||||
targets: [
|
targets: [
|
||||||
.target(
|
.target(
|
||||||
@ -91,3 +91,4 @@ XuqmSDK.shared.initialize(config: config)
|
|||||||
- [iOS IM 接入 →](./im)
|
- [iOS IM 接入 →](./im)
|
||||||
- [iOS Push 接入 →](./push)
|
- [iOS Push 接入 →](./push)
|
||||||
- [iOS 版本更新 →](./update)
|
- [iOS 版本更新 →](./update)
|
||||||
|
- [iOS 授权管理 →](./license)
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
# React Native SDK 接入指南
|
# React Native SDK 接入指南
|
||||||
|
|
||||||
**包名**:`@xuqm/rn-sdk` · **版本**:0.3.x(内部基础包,业务方不直接引用)
|
**包名**:`@xuqm/rn-sdk` · **版本**:0.2.x(内部基础包,业务方不直接引用)
|
||||||
|
|
||||||
> **注意**:`rn-sdk` 作为内部基础包存在,业务方正常接入时使用 `@xuqm/rn-common` 和各业务模块即可。
|
> **注意**:`rn-sdk` 作为内部基础包存在,业务方正常接入时使用 `@xuqm/rn-common` 和各业务模块即可。
|
||||||
|
|
||||||
@ -14,6 +14,7 @@
|
|||||||
| `@xuqm/rn-im` | 单聊、群聊、消息收发、本地 DB(WatermelonDB)|
|
| `@xuqm/rn-im` | 单聊、群聊、消息收发、本地 DB(WatermelonDB)|
|
||||||
| `@xuqm/rn-push` | 推送设备 Token 上报 |
|
| `@xuqm/rn-push` | 推送设备 Token 上报 |
|
||||||
| `@xuqm/rn-update` | App 版本检查、RN Bundle 热更新 |
|
| `@xuqm/rn-update` | App 版本检查、RN Bundle 热更新 |
|
||||||
|
| `@xuqm/rn-license` | 设备授权注册与验证(License SDK)|
|
||||||
| `@xuqm/rn-sdk` | 内部基础包,随 IM / Push / Update 自动安装,不建议业务方直接引用 |
|
| `@xuqm/rn-sdk` | 内部基础包,随 IM / Push / Update 自动安装,不建议业务方直接引用 |
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -23,7 +24,7 @@
|
|||||||
在项目根目录创建 `.npmrc`:
|
在项目根目录创建 `.npmrc`:
|
||||||
|
|
||||||
```
|
```
|
||||||
@xuqm:registry=https://nexus.xuqinmin.com/repository/npm/
|
@xuqm:registry=https://nexus.xuqinmin.com/repository/npm-hosted/
|
||||||
```
|
```
|
||||||
|
|
||||||
只使用基础能力时,直接安装 `rn-common`,不会带入 IM / Push / Update:
|
只使用基础能力时,直接安装 `rn-common`,不会带入 IM / Push / Update:
|
||||||
@ -42,7 +43,7 @@ yarn add @xuqm/rn-common @xuqm/rn-im
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 快速接入(当前 v0.3.x)
|
## 快速接入(当前 v0.2.x)
|
||||||
|
|
||||||
### 1. 初始化
|
### 1. 初始化
|
||||||
|
|
||||||
@ -271,8 +272,6 @@ await ImSDK.login(userId, userSig)
|
|||||||
// dbName 自动由 appKey + userId 派生,无需传入
|
// dbName 自动由 appKey + userId 派生,无需传入
|
||||||
```
|
```
|
||||||
|
|
||||||
UserSig 生成方式见 [安全设计文档](../../design/02-security-design.md)。
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 常见问题
|
## 常见问题
|
||||||
|
|||||||
190
docs-site/docs/rn/license.md
普通文件
190
docs-site/docs/rn/license.md
普通文件
@ -0,0 +1,190 @@
|
|||||||
|
# React Native 授权管理(License SDK)
|
||||||
|
|
||||||
|
**包名**:`@xuqm/rn-license` · **依赖**:`react-native-quick-crypto`(peer dep)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn add @xuqm/rn-license react-native-quick-crypto
|
||||||
|
```
|
||||||
|
|
||||||
|
iOS 需要执行 `pod install`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ios && pod install
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. 放置 License 文件
|
||||||
|
|
||||||
|
从租户平台下载 `.xuqmlicense` 加密文件,通过 `react-native-raw-text` 或 `require()` 将其作为文本资源嵌入 App。
|
||||||
|
|
||||||
|
**方式一:require 字符串资源(推荐)**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
// 将 license.xuqm 放入 src/assets/
|
||||||
|
const licenseContent = require('./assets/license.xuqm')
|
||||||
|
```
|
||||||
|
|
||||||
|
**方式二:手动初始化**
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { initialize } from '@xuqm/rn-license'
|
||||||
|
|
||||||
|
initialize('your_app_key', {
|
||||||
|
baseUrl: 'https://auth.dev.xuqinmin.com',
|
||||||
|
})
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 检查授权
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { initializeFromFile, checkLicense } from '@xuqm/rn-license'
|
||||||
|
|
||||||
|
// 启动时从加密文件自动初始化
|
||||||
|
const licenseContent = require('./assets/license.xuqm')
|
||||||
|
await initializeFromFile(licenseContent)
|
||||||
|
|
||||||
|
// 检查授权(有缓存时直接返回,否则网络验证)
|
||||||
|
const result = await checkLicense()
|
||||||
|
if (result.type === 'success') {
|
||||||
|
console.log('授权通过:', result.reason)
|
||||||
|
} else {
|
||||||
|
console.warn('授权失败:', result.message)
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 携带用户信息
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import type { LicenseUserInfo } from '@xuqm/rn-license'
|
||||||
|
|
||||||
|
const userInfo: LicenseUserInfo = {
|
||||||
|
userId: 'user_001',
|
||||||
|
name: '张三',
|
||||||
|
email: 'zhangsan@company.com',
|
||||||
|
}
|
||||||
|
|
||||||
|
const result = await checkLicense(userInfo)
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. API 说明
|
||||||
|
|
||||||
|
### initialize
|
||||||
|
|
||||||
|
```ts
|
||||||
|
initialize(appKey: string, options?: { baseUrl?: string; deviceName?: string }): void
|
||||||
|
```
|
||||||
|
|
||||||
|
手动初始化,适用于不使用 License 文件的场景。
|
||||||
|
|
||||||
|
### initializeFromFile
|
||||||
|
|
||||||
|
```ts
|
||||||
|
initializeFromFile(encryptedContent: string): Promise<void>
|
||||||
|
```
|
||||||
|
|
||||||
|
从加密 License 文件内容自动解析 appKey 和 baseUrl 并初始化。
|
||||||
|
|
||||||
|
### checkLicense
|
||||||
|
|
||||||
|
```ts
|
||||||
|
checkLicense(userInfo?: LicenseUserInfo): Promise<LicenseResult>
|
||||||
|
```
|
||||||
|
|
||||||
|
返回 `{ type: 'success', reason: string }` 或 `{ type: 'error', message: string }`。
|
||||||
|
|
||||||
|
**缓存策略**:10 分钟有效期,有效期内直接返回缓存结果(不发起网络请求)。
|
||||||
|
|
||||||
|
### getStatus
|
||||||
|
|
||||||
|
```ts
|
||||||
|
getStatus(): Promise<LicenseStatus> // 'ok' | 'denied' | 'unknown'
|
||||||
|
```
|
||||||
|
|
||||||
|
### getDeviceId
|
||||||
|
|
||||||
|
```ts
|
||||||
|
getDeviceId(): Promise<string>
|
||||||
|
```
|
||||||
|
|
||||||
|
### clear
|
||||||
|
|
||||||
|
```ts
|
||||||
|
clear(): Promise<void>
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 数据存储
|
||||||
|
|
||||||
|
| 数据 | 存储方式 |
|
||||||
|
|------|---------|
|
||||||
|
| deviceId | AsyncStorage |
|
||||||
|
| token | AsyncStorage |
|
||||||
|
| 授权状态 | AsyncStorage |
|
||||||
|
| statusTime | AsyncStorage |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 离线模式
|
||||||
|
|
||||||
|
- 首次激活需要网络连接
|
||||||
|
- 激活后 10 分钟缓存内可离线使用
|
||||||
|
- 网络异常时,若历史缓存成功,继续返回授权通过
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. 完整示例
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import React, { useEffect, useState } from 'react'
|
||||||
|
import { View, Text } from 'react-native'
|
||||||
|
import { initializeFromFile, checkLicense } from '@xuqm/rn-license'
|
||||||
|
|
||||||
|
export default function App() {
|
||||||
|
const [licensed, setLicensed] = useState<boolean | null>(null)
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
async function verify() {
|
||||||
|
const content = require('./assets/license.xuqm')
|
||||||
|
await initializeFromFile(content)
|
||||||
|
const result = await checkLicense({ userId: 'user_001' })
|
||||||
|
setLicensed(result.type === 'success')
|
||||||
|
}
|
||||||
|
verify()
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
if (licensed === null) return <Text>验证中...</Text>
|
||||||
|
if (!licensed) return <Text>授权验证失败,请联系管理员</Text>
|
||||||
|
return <View>{ /* 主界面 */ }</View>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 常见问题
|
||||||
|
|
||||||
|
**Q: 提示 `react-native-quick-crypto not available`?**
|
||||||
|
确认已安装 `react-native-quick-crypto` 并执行了 `pod install`(iOS)或重新 build(Android)。
|
||||||
|
|
||||||
|
**Q: License 文件如何用 require 加载?**
|
||||||
|
需要在 Metro 配置中将 `.xuqm` 加入 `assetExts`:
|
||||||
|
|
||||||
|
```js
|
||||||
|
// metro.config.js
|
||||||
|
const config = {
|
||||||
|
resolver: {
|
||||||
|
assetExts: [...defaultConfig.resolver.assetExts, 'xuqm'],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
```
|
||||||
138
docs-site/docs/rn/push.md
普通文件
138
docs-site/docs/rn/push.md
普通文件
@ -0,0 +1,138 @@
|
|||||||
|
# React Native 推送接入指南
|
||||||
|
|
||||||
|
**包名**:`@xuqm/rn-push` · **支持**:华为、小米、OPPO、vivo、荣耀、APNs(iOS)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. 安装
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn add @xuqm/rn-push
|
||||||
|
```
|
||||||
|
|
||||||
|
iOS 需要执行 `pod install`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd ios && pod install
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Android 厂商推送集成
|
||||||
|
|
||||||
|
各厂商推送 SDK 需在原生 Android 层集成。在 `android/app/build.gradle` 中按需添加:
|
||||||
|
|
||||||
|
```gradle
|
||||||
|
dependencies {
|
||||||
|
// 华为 HMS Push
|
||||||
|
implementation 'com.huawei.hms:push:6.9.0.300'
|
||||||
|
// 小米 Push
|
||||||
|
implementation 'com.xiaomi.mipush:sdk:5.0.6'
|
||||||
|
// OPPO Push
|
||||||
|
implementation 'com.heytap.msp:push:3.5.0'
|
||||||
|
// vivo Push
|
||||||
|
implementation 'com.vivo.push:sdk:3.0.0.4_484'
|
||||||
|
// 荣耀 Push
|
||||||
|
implementation 'com.hihonor.mcs:push:7.0.41.301'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. 请求原生推送权限并注册
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { PushSDK } from '@xuqm/rn-push'
|
||||||
|
|
||||||
|
// 触发原生推送注册(Android 请求厂商 token;iOS 请求 APNs 权限)
|
||||||
|
await PushSDK.requestNativeRegistration()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. 监听推送 Token
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { PushSDK } from '@xuqm/rn-push'
|
||||||
|
|
||||||
|
// 在 App 启动时监听 token 回调,并向服务端注册
|
||||||
|
const unsubscribe = PushSDK.onPushToken(async (token, vendor) => {
|
||||||
|
console.log('获取到 Token:', token, '厂商:', vendor)
|
||||||
|
// 登录后调用注册接口
|
||||||
|
await PushSDK.setDeviceToken(token, vendor as PushVendor)
|
||||||
|
})
|
||||||
|
|
||||||
|
// 在组件卸载时取消监听
|
||||||
|
unsubscribe()
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. 手动注册 Token
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { PushSDK } from '@xuqm/rn-push'
|
||||||
|
import type { PushVendor } from '@xuqm/rn-push'
|
||||||
|
|
||||||
|
// 登录成功后,将 token 注册到服务端
|
||||||
|
await PushSDK.registerToken('user_001', 'device_token_here', 'HUAWEI')
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. 登出时注销 Token
|
||||||
|
|
||||||
|
```ts
|
||||||
|
await PushSDK.unregisterToken('user_001')
|
||||||
|
// 或简写
|
||||||
|
await PushSDK.logout('user_001')
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. 多模块统一登录
|
||||||
|
|
||||||
|
Push 模块与 IM、Update 模块共享同一套登录态:
|
||||||
|
|
||||||
|
```ts
|
||||||
|
import { XuqmSDK } from '@xuqm/rn-common'
|
||||||
|
import { PushSDK } from '@xuqm/rn-push'
|
||||||
|
|
||||||
|
await XuqmSDK.initialize({ appKey: 'your_app_key' })
|
||||||
|
await XuqmSDK.login({ userId: 'user_001', userSig: 'your_user_sig' })
|
||||||
|
// ↓ 登录后调用 PushSDK.initialize() 完成 token 注册
|
||||||
|
await PushSDK.initialize('user_001')
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. iOS APNs 配置
|
||||||
|
|
||||||
|
在 `AppDelegate.m` 或 `AppDelegate.swift` 中:
|
||||||
|
|
||||||
|
```objc
|
||||||
|
// AppDelegate.m
|
||||||
|
- (void)application:(UIApplication *)application
|
||||||
|
didRegisterForRemoteNotificationsWithDeviceToken:(NSData *)deviceToken {
|
||||||
|
// 转发 token,由 rn-push 原生模块处理
|
||||||
|
[RCTEventEmitter ...] // 通过 Bridge 传至 JS
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
> 使用 `PushSDK.requestNativeRegistration()` 会自动触发 iOS APNs 注册流程,无需额外原生代码(前提是 RN 0.76+ 自动链接)。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. 厂商渠道自动检测
|
||||||
|
|
||||||
|
`@xuqm/rn-common` 的 `detectPushVendor` 会根据 `device.brand` 自动识别厂商:
|
||||||
|
|
||||||
|
| 品牌关键字 | 识别厂商 |
|
||||||
|
|-----------|---------|
|
||||||
|
| xiaomi / redmi | XIAOMI |
|
||||||
|
| huawei | HUAWEI |
|
||||||
|
| honor | HONOR |
|
||||||
|
| oppo / realme | OPPO |
|
||||||
|
| vivo / iqoo | VIVO |
|
||||||
|
| iOS | APNS |
|
||||||
|
| 其他 | FCM |
|
||||||
@ -96,7 +96,8 @@ allprojects {
|
|||||||
├── @xuqm/rn-common ← 初始化、网络、设备信息
|
├── @xuqm/rn-common ← 初始化、网络、设备信息
|
||||||
├── @xuqm/rn-im ← IM 模块(依赖 WatermelonDB)
|
├── @xuqm/rn-im ← IM 模块(依赖 WatermelonDB)
|
||||||
├── @xuqm/rn-push ← Push 模块
|
├── @xuqm/rn-push ← Push 模块
|
||||||
└── @xuqm/rn-update ← 更新模块
|
├── @xuqm/rn-update ← 更新模块
|
||||||
|
└── @xuqm/rn-license ← 授权管理模块(依赖 react-native-quick-crypto)
|
||||||
```
|
```
|
||||||
|
|
||||||
---
|
---
|
||||||
@ -105,4 +106,6 @@ allprojects {
|
|||||||
|
|
||||||
- [RN IM 接入 →](./im)
|
- [RN IM 接入 →](./im)
|
||||||
- [RN 群聊 →](./group)
|
- [RN 群聊 →](./group)
|
||||||
|
- [RN 推送接入 →](./push)
|
||||||
- [RN 版本更新 →](./update)
|
- [RN 版本更新 →](./update)
|
||||||
|
- [RN 授权管理 →](./license)
|
||||||
|
|||||||
1
ops-platform/components.d.ts
vendored
1
ops-platform/components.d.ts
vendored
@ -12,6 +12,7 @@ declare module 'vue' {
|
|||||||
ElCard: typeof import('element-plus/es')['ElCard']
|
ElCard: typeof import('element-plus/es')['ElCard']
|
||||||
ElCol: typeof import('element-plus/es')['ElCol']
|
ElCol: typeof import('element-plus/es')['ElCol']
|
||||||
ElContainer: typeof import('element-plus/es')['ElContainer']
|
ElContainer: typeof import('element-plus/es')['ElContainer']
|
||||||
|
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
|
||||||
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
|
ElDescriptions: typeof import('element-plus/es')['ElDescriptions']
|
||||||
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
|
ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem']
|
||||||
ElDialog: typeof import('element-plus/es')['ElDialog']
|
ElDialog: typeof import('element-plus/es')['ElDialog']
|
||||||
|
|||||||
@ -198,4 +198,9 @@ export const appApi = {
|
|||||||
|
|
||||||
downloadLicenseFile: (appKey: string) =>
|
downloadLicenseFile: (appKey: string) =>
|
||||||
client.get<Blob>(`/apps/${appKey}/license-file`, { responseType: 'blob' }),
|
client.get<Blob>(`/apps/${appKey}/license-file`, { responseType: 'blob' }),
|
||||||
|
|
||||||
|
requestActivation: (appKey: string, serviceType: 'IM' | 'PUSH' | 'UPDATE' | 'LICENSE', reason: string) =>
|
||||||
|
client.post<{ data: null }>(`/apps/${appKey}/services/request-activation`, null, {
|
||||||
|
params: { platform: 'ANDROID', serviceType, applyReason: reason },
|
||||||
|
}),
|
||||||
}
|
}
|
||||||
|
|||||||
@ -9,7 +9,27 @@
|
|||||||
<el-page-header v-else @back="$router.back()" :content="`即时通讯管理 — ${appKey}`" style="margin-bottom:20px" />
|
<el-page-header v-else @back="$router.back()" :content="`即时通讯管理 — ${appKey}`" style="margin-bottom:20px" />
|
||||||
<el-empty v-if="isServicesPortal && !appKey" description="请选择一个应用" style="margin-top:80px" />
|
<el-empty v-if="isServicesPortal && !appKey" description="请选择一个应用" style="margin-top:80px" />
|
||||||
|
|
||||||
<template v-if="!isServicesPortal || appKey">
|
<div v-if="isServicesPortal && appKey && checkingService" v-loading="true" style="min-height:200px" />
|
||||||
|
|
||||||
|
<template v-if="isServicesPortal && appKey && serviceEnabled === false">
|
||||||
|
<el-empty :image-size="80" description="当前应用未开通即时通讯服务" style="margin-top:60px">
|
||||||
|
<el-button type="primary" @click="showActivationDialog = true">申请开通</el-button>
|
||||||
|
</el-empty>
|
||||||
|
<el-dialog v-model="showActivationDialog" title="申请开通即时通讯" width="460px">
|
||||||
|
<el-form label-width="80px">
|
||||||
|
<el-form-item label="服务">即时通讯</el-form-item>
|
||||||
|
<el-form-item label="申请理由">
|
||||||
|
<el-input v-model="activationReason" type="textarea" :rows="3" placeholder="请描述您的业务场景" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="showActivationDialog = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="submittingActivation" @click="submitActivation">提交申请</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="!isServicesPortal || (appKey && serviceEnabled === true)">
|
||||||
<el-row :gutter="16" class="stat-grid">
|
<el-row :gutter="16" class="stat-grid">
|
||||||
<el-col :xs="24" :sm="12" :md="6" v-for="item in statCards" :key="item.label">
|
<el-col :xs="24" :sm="12" :md="6" v-for="item in statCards" :key="item.label">
|
||||||
<el-card shadow="never">
|
<el-card shadow="never">
|
||||||
@ -794,7 +814,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onMounted, ref } from 'vue'
|
import { computed, onMounted, ref, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { appApi, type App } from '@/api/app'
|
import { appApi, type App } from '@/api/app'
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
@ -825,6 +845,48 @@ const appKey = computed(() => {
|
|||||||
return String(route.params['appKey'] ?? '')
|
return String(route.params['appKey'] ?? '')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const serviceEnabled = ref<boolean | null>(null)
|
||||||
|
const checkingService = ref(false)
|
||||||
|
const showActivationDialog = ref(false)
|
||||||
|
const activationReason = ref('')
|
||||||
|
const submittingActivation = ref(false)
|
||||||
|
|
||||||
|
async function checkServiceEnabled(key: string) {
|
||||||
|
checkingService.value = true
|
||||||
|
serviceEnabled.value = null
|
||||||
|
try {
|
||||||
|
const res = await appApi.getServices(key)
|
||||||
|
const enabled = res.data.data.some(s => s.serviceType === 'IM' && s.enabled)
|
||||||
|
serviceEnabled.value = enabled
|
||||||
|
if (enabled) {
|
||||||
|
loadStats()
|
||||||
|
loadUsers()
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
serviceEnabled.value = false
|
||||||
|
} finally {
|
||||||
|
checkingService.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitActivation() {
|
||||||
|
if (!activationReason.value.trim()) {
|
||||||
|
ElMessage.warning('请填写申请理由')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
submittingActivation.value = true
|
||||||
|
try {
|
||||||
|
await appApi.requestActivation(appKey.value, 'IM', activationReason.value.trim())
|
||||||
|
ElMessage.success('申请已提交,等待运营审核')
|
||||||
|
showActivationDialog.value = false
|
||||||
|
activationReason.value = ''
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('提交失败')
|
||||||
|
} finally {
|
||||||
|
submittingActivation.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const genderLabel: Record<string, string> = {
|
const genderLabel: Record<string, string> = {
|
||||||
UNKNOWN: '未知',
|
UNKNOWN: '未知',
|
||||||
MALE: '男',
|
MALE: '男',
|
||||||
@ -1913,10 +1975,18 @@ function handleOperationLogPageChange(page: number) {
|
|||||||
loadOperationLogs(page - 1)
|
loadOperationLogs(page - 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(appKey, (key) => {
|
||||||
|
if (isServicesPortal.value && key) {
|
||||||
|
checkServiceEnabled(key)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (isServicesPortal.value) {
|
if (isServicesPortal.value) {
|
||||||
appApi.list().then(res => { portalApps.value = res.data.data })
|
appApi.list().then(res => { portalApps.value = res.data.data })
|
||||||
if (!appKey.value) return
|
if (!appKey.value) return
|
||||||
|
checkServiceEnabled(appKey.value)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
loadStats()
|
loadStats()
|
||||||
loadUsers()
|
loadUsers()
|
||||||
|
|||||||
@ -9,7 +9,27 @@
|
|||||||
<el-page-header v-else @back="$router.back()" content="推送管理" style="margin-bottom:24px" />
|
<el-page-header v-else @back="$router.back()" content="推送管理" style="margin-bottom:24px" />
|
||||||
<el-empty v-if="isServicesPortal && !appKey" description="请选择一个应用" style="margin-top:80px" />
|
<el-empty v-if="isServicesPortal && !appKey" description="请选择一个应用" style="margin-top:80px" />
|
||||||
|
|
||||||
<template v-if="!isServicesPortal || appKey">
|
<div v-if="isServicesPortal && appKey && checkingService" v-loading="true" style="min-height:200px" />
|
||||||
|
|
||||||
|
<template v-if="isServicesPortal && appKey && serviceEnabled === false">
|
||||||
|
<el-empty :image-size="80" description="当前应用未开通离线推送服务" style="margin-top:60px">
|
||||||
|
<el-button type="primary" @click="showActivationDialog = true">申请开通</el-button>
|
||||||
|
</el-empty>
|
||||||
|
<el-dialog v-model="showActivationDialog" title="申请开通离线推送" width="460px">
|
||||||
|
<el-form label-width="80px">
|
||||||
|
<el-form-item label="服务">离线推送</el-form-item>
|
||||||
|
<el-form-item label="申请理由">
|
||||||
|
<el-input v-model="activationReason" type="textarea" :rows="3" placeholder="请描述您的业务场景" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="showActivationDialog = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="submittingActivation" @click="submitActivation">提交申请</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="!isServicesPortal || (appKey && serviceEnabled === true)">
|
||||||
<el-card style="margin-bottom:16px">
|
<el-card style="margin-bottom:16px">
|
||||||
<template #header>用户设备状态查询</template>
|
<template #header>用户设备状态查询</template>
|
||||||
<el-form inline @submit.prevent="queryUser">
|
<el-form inline @submit.prevent="queryUser">
|
||||||
@ -148,7 +168,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, reactive, ref, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage } from 'element-plus'
|
||||||
import { appApi, type App } from '@/api/app'
|
import { appApi, type App } from '@/api/app'
|
||||||
@ -156,10 +176,47 @@ import { pushAdminApi, type DeviceLoginLog, type TestPushResult, type UserPushSt
|
|||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const appKey = route.params.appKey as string
|
const appKey = computed(() => (route.params.appKey as string) ?? '')
|
||||||
const isServicesPortal = computed(() => route.path.startsWith('/services/'))
|
const isServicesPortal = computed(() => route.path.startsWith('/services/'))
|
||||||
const portalApps = ref<App[]>([])
|
const portalApps = ref<App[]>([])
|
||||||
|
|
||||||
|
const serviceEnabled = ref<boolean | null>(null)
|
||||||
|
const checkingService = ref(false)
|
||||||
|
const showActivationDialog = ref(false)
|
||||||
|
const activationReason = ref('')
|
||||||
|
const submittingActivation = ref(false)
|
||||||
|
|
||||||
|
async function checkServiceEnabled() {
|
||||||
|
checkingService.value = true
|
||||||
|
serviceEnabled.value = null
|
||||||
|
try {
|
||||||
|
const res = await appApi.getServices(appKey.value)
|
||||||
|
serviceEnabled.value = res.data.data.some(s => s.serviceType === 'PUSH' && s.enabled)
|
||||||
|
} catch {
|
||||||
|
serviceEnabled.value = false
|
||||||
|
} finally {
|
||||||
|
checkingService.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitActivation() {
|
||||||
|
if (!activationReason.value.trim()) {
|
||||||
|
ElMessage.warning('请填写申请理由')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
submittingActivation.value = true
|
||||||
|
try {
|
||||||
|
await appApi.requestActivation(appKey.value, 'PUSH', activationReason.value.trim())
|
||||||
|
ElMessage.success('申请已提交,等待运营审核')
|
||||||
|
showActivationDialog.value = false
|
||||||
|
activationReason.value = ''
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('提交失败')
|
||||||
|
} finally {
|
||||||
|
submittingActivation.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const isMobile = ref(window.innerWidth < 768)
|
const isMobile = ref(window.innerWidth < 768)
|
||||||
function updateViewport() { isMobile.value = window.innerWidth < 768 }
|
function updateViewport() { isMobile.value = window.innerWidth < 768 }
|
||||||
|
|
||||||
@ -192,7 +249,7 @@ async function queryUser() {
|
|||||||
querying.value = true
|
querying.value = true
|
||||||
testResult.value = null
|
testResult.value = null
|
||||||
try {
|
try {
|
||||||
const res = await pushAdminApi.getUserStatus(appKey, uid)
|
const res = await pushAdminApi.getUserStatus(appKey.value, uid)
|
||||||
userStatus.value = res.data.data
|
userStatus.value = res.data.data
|
||||||
logsUserId.value = uid
|
logsUserId.value = uid
|
||||||
logsPage.value = 1
|
logsPage.value = 1
|
||||||
@ -210,7 +267,7 @@ async function sendTestPush() {
|
|||||||
testResult.value = null
|
testResult.value = null
|
||||||
try {
|
try {
|
||||||
const res = await pushAdminApi.testOffline(
|
const res = await pushAdminApi.testOffline(
|
||||||
appKey,
|
appKey.value,
|
||||||
userStatus.value.userId,
|
userStatus.value.userId,
|
||||||
testForm.title,
|
testForm.title,
|
||||||
testForm.body,
|
testForm.body,
|
||||||
@ -229,7 +286,7 @@ async function loadLogs() {
|
|||||||
if (!uid) return
|
if (!uid) return
|
||||||
logsLoading.value = true
|
logsLoading.value = true
|
||||||
try {
|
try {
|
||||||
const res = await pushAdminApi.getDeviceLogs(appKey, uid, logsPage.value - 1, logsPageSize)
|
const res = await pushAdminApi.getDeviceLogs(appKey.value, uid, logsPage.value - 1, logsPageSize)
|
||||||
const d = res.data.data
|
const d = res.data.data
|
||||||
logs.value = d.content
|
logs.value = d.content
|
||||||
logsTotal.value = d.total
|
logsTotal.value = d.total
|
||||||
@ -251,9 +308,16 @@ function formatDateTime(iso: string): string {
|
|||||||
return new Date(iso).toLocaleString('zh-CN')
|
return new Date(iso).toLocaleString('zh-CN')
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(appKey, (key) => {
|
||||||
|
if (isServicesPortal.value && key) checkServiceEnabled()
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
if (isServicesPortal.value) {
|
if (isServicesPortal.value) {
|
||||||
appApi.list().then(res => { portalApps.value = res.data.data })
|
appApi.list().then(res => { portalApps.value = res.data.data })
|
||||||
|
if (appKey.value) {
|
||||||
|
checkServiceEnabled()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
window.addEventListener('resize', updateViewport)
|
window.addEventListener('resize', updateViewport)
|
||||||
})
|
})
|
||||||
|
|||||||
@ -14,7 +14,27 @@
|
|||||||
<el-page-header v-else @back="$router.back()" :content="`版本管理 — ${pageTitle}`" style="margin-bottom:20px" />
|
<el-page-header v-else @back="$router.back()" :content="`版本管理 — ${pageTitle}`" style="margin-bottom:20px" />
|
||||||
<el-empty v-if="isServicesPortal && !appKey" description="请选择一个应用" style="margin-top:80px" />
|
<el-empty v-if="isServicesPortal && !appKey" description="请选择一个应用" style="margin-top:80px" />
|
||||||
|
|
||||||
<template v-if="!isServicesPortal || appKey">
|
<div v-if="isServicesPortal && appKey && checkingService" v-loading="true" style="min-height:200px" />
|
||||||
|
|
||||||
|
<template v-if="isServicesPortal && appKey && serviceEnabled === false">
|
||||||
|
<el-empty :image-size="80" description="当前应用未开通版本管理服务" style="margin-top:60px">
|
||||||
|
<el-button type="primary" @click="showActivationDialog = true">申请开通</el-button>
|
||||||
|
</el-empty>
|
||||||
|
<el-dialog v-model="showActivationDialog" title="申请开通版本管理" width="460px">
|
||||||
|
<el-form label-width="80px">
|
||||||
|
<el-form-item label="服务">版本管理</el-form-item>
|
||||||
|
<el-form-item label="申请理由">
|
||||||
|
<el-input v-model="activationReason" type="textarea" :rows="3" placeholder="请描述您的业务场景" />
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="showActivationDialog = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="submittingActivation" @click="submitActivation">提交申请</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<template v-if="!isServicesPortal || (appKey && serviceEnabled === true)">
|
||||||
<el-card>
|
<el-card>
|
||||||
<el-tabs v-model="activeTab">
|
<el-tabs v-model="activeTab">
|
||||||
<!-- App Versions -->
|
<!-- App Versions -->
|
||||||
@ -840,11 +860,48 @@ import honorGuideImage from '@/assets/update-store/honor/01.png'
|
|||||||
|
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
const appKey = route.params.appKey as string
|
const appKey = computed(() => (route.params.appKey as string) ?? '')
|
||||||
const isServicesPortal = computed(() => route.path.startsWith('/services/'))
|
const isServicesPortal = computed(() => route.path.startsWith('/services/'))
|
||||||
const portalApps = ref<App[]>([])
|
const portalApps = ref<App[]>([])
|
||||||
|
|
||||||
|
const serviceEnabled = ref<boolean | null>(null)
|
||||||
|
const checkingService = ref(false)
|
||||||
|
const showActivationDialog = ref(false)
|
||||||
|
const activationReason = ref('')
|
||||||
|
const submittingActivation = ref(false)
|
||||||
|
|
||||||
|
async function checkServiceEnabled() {
|
||||||
|
checkingService.value = true
|
||||||
|
serviceEnabled.value = null
|
||||||
|
try {
|
||||||
|
const res = await appApi.getServices(appKey.value)
|
||||||
|
serviceEnabled.value = res.data.data.some(s => s.serviceType === 'UPDATE' && s.enabled)
|
||||||
|
} catch {
|
||||||
|
serviceEnabled.value = false
|
||||||
|
} finally {
|
||||||
|
checkingService.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function submitActivation() {
|
||||||
|
if (!activationReason.value.trim()) {
|
||||||
|
ElMessage.warning('请填写申请理由')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
submittingActivation.value = true
|
||||||
|
try {
|
||||||
|
await appApi.requestActivation(appKey.value, 'UPDATE', activationReason.value.trim())
|
||||||
|
ElMessage.success('申请已提交,等待运营审核')
|
||||||
|
showActivationDialog.value = false
|
||||||
|
activationReason.value = ''
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('提交失败')
|
||||||
|
} finally {
|
||||||
|
submittingActivation.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
const app = ref<App | null>(null)
|
const app = ref<App | null>(null)
|
||||||
const pageTitle = computed(() => app.value?.name ?? appKey)
|
const pageTitle = computed(() => app.value?.name ?? appKey.value)
|
||||||
const isMobile = ref(false)
|
const isMobile = ref(false)
|
||||||
const dialogWidth = computed(() => (isMobile.value ? 'calc(100vw - 24px)' : '920px'))
|
const dialogWidth = computed(() => (isMobile.value ? 'calc(100vw - 24px)' : '920px'))
|
||||||
|
|
||||||
@ -1160,7 +1217,7 @@ async function toggleStore(type: StoreType, enabled: boolean) {
|
|||||||
const cfg = getStoreConfig(type)
|
const cfg = getStoreConfig(type)
|
||||||
if (!cfg) return
|
if (!cfg) return
|
||||||
try {
|
try {
|
||||||
await updateAdminApi.saveStoreConfig(appKey, type, cfg.configJson ?? '{}', enabled)
|
await updateAdminApi.saveStoreConfig(appKey.value, type, cfg.configJson ?? '{}', enabled)
|
||||||
await loadStoreConfigs()
|
await loadStoreConfigs()
|
||||||
} catch {
|
} catch {
|
||||||
ElMessage.error('操作失败')
|
ElMessage.error('操作失败')
|
||||||
@ -1169,7 +1226,7 @@ async function toggleStore(type: StoreType, enabled: boolean) {
|
|||||||
|
|
||||||
async function loadStoreConfigs() {
|
async function loadStoreConfigs() {
|
||||||
try {
|
try {
|
||||||
const res = await updateAdminApi.getStoreConfigs(appKey)
|
const res = await updateAdminApi.getStoreConfigs(appKey.value)
|
||||||
storeConfigs.value = res.data.data
|
storeConfigs.value = res.data.data
|
||||||
} catch {
|
} catch {
|
||||||
storeConfigs.value = []
|
storeConfigs.value = []
|
||||||
@ -1181,7 +1238,7 @@ function switchApp(val: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function loadApp() {
|
async function loadApp() {
|
||||||
const res = await appApi.get(appKey)
|
const res = await appApi.get(appKey.value)
|
||||||
app.value = res.data.data
|
app.value = res.data.data
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1220,7 +1277,7 @@ async function saveStoreConfig() {
|
|||||||
savingStoreConfig.value = true
|
savingStoreConfig.value = true
|
||||||
try {
|
try {
|
||||||
await updateAdminApi.saveStoreConfig(
|
await updateAdminApi.saveStoreConfig(
|
||||||
appKey,
|
appKey.value,
|
||||||
currentStoreDef.value.type,
|
currentStoreDef.value.type,
|
||||||
JSON.stringify(storeConfigForm.value.values),
|
JSON.stringify(storeConfigForm.value.values),
|
||||||
storeConfigForm.value.enabled,
|
storeConfigForm.value.enabled,
|
||||||
@ -1238,7 +1295,7 @@ async function saveStoreConfig() {
|
|||||||
async function removeStoreConfig(type: StoreType) {
|
async function removeStoreConfig(type: StoreType) {
|
||||||
await ElMessageBox.confirm('确认删除此应用商店凭据?', '提示', { type: 'warning' })
|
await ElMessageBox.confirm('确认删除此应用商店凭据?', '提示', { type: 'warning' })
|
||||||
try {
|
try {
|
||||||
await updateAdminApi.deleteStoreConfig(appKey, type)
|
await updateAdminApi.deleteStoreConfig(appKey.value, type)
|
||||||
ElMessage.success('已删除')
|
ElMessage.success('已删除')
|
||||||
await loadStoreConfigs()
|
await loadStoreConfigs()
|
||||||
} catch {
|
} catch {
|
||||||
@ -1279,7 +1336,7 @@ function parsePublishConfig(config?: string | null) {
|
|||||||
async function loadPublishConfig() {
|
async function loadPublishConfig() {
|
||||||
loadingPublishConfig.value = true
|
loadingPublishConfig.value = true
|
||||||
try {
|
try {
|
||||||
const res = await updateAdminApi.getPublishConfig(appKey)
|
const res = await updateAdminApi.getPublishConfig(appKey.value)
|
||||||
publishConfigForm.value = parsePublishConfig(res.data.data.configJson)
|
publishConfigForm.value = parsePublishConfig(res.data.data.configJson)
|
||||||
if (allowAnonymousUpdateCheck.value) {
|
if (allowAnonymousUpdateCheck.value) {
|
||||||
publishConfigForm.value.grayMode = 'PERCENT'
|
publishConfigForm.value.grayMode = 'PERCENT'
|
||||||
@ -1319,7 +1376,7 @@ async function savePublishConfig() {
|
|||||||
if (payload.grayMode === 'MEMBERS' && !hasGrayDirectorySyncCallback.value) {
|
if (payload.grayMode === 'MEMBERS' && !hasGrayDirectorySyncCallback.value) {
|
||||||
payload.graySelectionSource = 'CALLBACK'
|
payload.graySelectionSource = 'CALLBACK'
|
||||||
}
|
}
|
||||||
await updateAdminApi.savePublishConfig(appKey, payload)
|
await updateAdminApi.savePublishConfig(appKey.value, payload)
|
||||||
ElMessage.success('发布配置已保存')
|
ElMessage.success('发布配置已保存')
|
||||||
} catch {
|
} catch {
|
||||||
ElMessage.error('保存失败')
|
ElMessage.error('保存失败')
|
||||||
@ -1479,7 +1536,7 @@ async function loadGrayMembers() {
|
|||||||
if (!showGray.value || !hasGrayDirectorySyncCallback.value) return
|
if (!showGray.value || !hasGrayDirectorySyncCallback.value) return
|
||||||
loadingGrayMembers.value = true
|
loadingGrayMembers.value = true
|
||||||
try {
|
try {
|
||||||
const res = await updateAdminApi.listGrayMembers(appKey, grayMemberKeyword.value || undefined, grayMemberGroupFilter.value || undefined)
|
const res = await updateAdminApi.listGrayMembers(appKey.value, grayMemberKeyword.value || undefined, grayMemberGroupFilter.value || undefined)
|
||||||
grayMembers.value = res.data.data
|
grayMembers.value = res.data.data
|
||||||
} catch {
|
} catch {
|
||||||
grayMembers.value = []
|
grayMembers.value = []
|
||||||
@ -1495,7 +1552,7 @@ async function syncGrayMembers() {
|
|||||||
}
|
}
|
||||||
loadingGrayMembers.value = true
|
loadingGrayMembers.value = true
|
||||||
try {
|
try {
|
||||||
const res = await updateAdminApi.syncGrayMembers(appKey)
|
const res = await updateAdminApi.syncGrayMembers(appKey.value)
|
||||||
grayMembers.value = res.data.data
|
grayMembers.value = res.data.data
|
||||||
ElMessage.success('成员已同步')
|
ElMessage.success('成员已同步')
|
||||||
} catch {
|
} catch {
|
||||||
@ -1673,7 +1730,7 @@ async function submitAppUpload() {
|
|||||||
appVersionUploadProgress.value = 0
|
appVersionUploadProgress.value = 0
|
||||||
try {
|
try {
|
||||||
const fd = new FormData()
|
const fd = new FormData()
|
||||||
fd.append('appKey', appKey)
|
fd.append('appKey', appKey.value)
|
||||||
fd.append('platform', f.platform)
|
fd.append('platform', f.platform)
|
||||||
fd.append('versionName', f.versionName)
|
fd.append('versionName', f.versionName)
|
||||||
fd.append('versionCode', String(f.versionCode))
|
fd.append('versionCode', String(f.versionCode))
|
||||||
@ -1767,7 +1824,7 @@ async function submitRnUpload() {
|
|||||||
rnBundleUploadProgress.value = 0
|
rnBundleUploadProgress.value = 0
|
||||||
try {
|
try {
|
||||||
const fd = new FormData()
|
const fd = new FormData()
|
||||||
fd.append('appKey', appKey)
|
fd.append('appKey', appKey.value)
|
||||||
fd.append('moduleId', f.moduleId)
|
fd.append('moduleId', f.moduleId)
|
||||||
fd.append('platform', f.platform)
|
fd.append('platform', f.platform)
|
||||||
fd.append('version', f.version)
|
fd.append('version', f.version)
|
||||||
@ -1791,7 +1848,7 @@ async function submitRnUpload() {
|
|||||||
async function loadAppVersions() {
|
async function loadAppVersions() {
|
||||||
loadingApp.value = true
|
loadingApp.value = true
|
||||||
try {
|
try {
|
||||||
const res = await updateAdminApi.listAppVersions(appKey, appPlatform.value)
|
const res = await updateAdminApi.listAppVersions(appKey.value, appPlatform.value)
|
||||||
appVersions.value = res.data.data
|
appVersions.value = res.data.data
|
||||||
} catch {
|
} catch {
|
||||||
ElMessage.error('加载失败')
|
ElMessage.error('加载失败')
|
||||||
@ -1803,7 +1860,7 @@ async function loadAppVersions() {
|
|||||||
async function loadRnBundles() {
|
async function loadRnBundles() {
|
||||||
loadingRn.value = true
|
loadingRn.value = true
|
||||||
try {
|
try {
|
||||||
const res = await updateAdminApi.listRnBundles(appKey, rnModuleFilter.value || undefined, rnPlatform.value || undefined)
|
const res = await updateAdminApi.listRnBundles(appKey.value, rnModuleFilter.value || undefined, rnPlatform.value || undefined)
|
||||||
rnBundles.value = res.data.data
|
rnBundles.value = res.data.data
|
||||||
} catch {
|
} catch {
|
||||||
ElMessage.error('加载失败')
|
ElMessage.error('加载失败')
|
||||||
@ -2046,7 +2103,7 @@ function updateViewport() {
|
|||||||
async function loadOperationLogs() {
|
async function loadOperationLogs() {
|
||||||
loadingOperationLogs.value = true
|
loadingOperationLogs.value = true
|
||||||
try {
|
try {
|
||||||
const res = await updateAdminApi.listOperationLogs(appKey, 100)
|
const res = await updateAdminApi.listOperationLogs(appKey.value, 100)
|
||||||
operationLogs.value = res.data.data
|
operationLogs.value = res.data.data
|
||||||
} catch {
|
} catch {
|
||||||
operationLogs.value = []
|
operationLogs.value = []
|
||||||
@ -2092,6 +2149,10 @@ function parseStoreReview(json?: string): { store: string; state: string; reason
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
watch(appKey, (key) => {
|
||||||
|
if (isServicesPortal.value && key) checkServiceEnabled()
|
||||||
|
})
|
||||||
|
|
||||||
watch(app, (value) => {
|
watch(app, (value) => {
|
||||||
if (value?.packageName) {
|
if (value?.packageName) {
|
||||||
appUploadForm.value.packageName = value.packageName
|
appUploadForm.value.packageName = value.packageName
|
||||||
@ -2124,7 +2185,8 @@ onMounted(() => {
|
|||||||
window.addEventListener('resize', updateViewport)
|
window.addEventListener('resize', updateViewport)
|
||||||
if (isServicesPortal.value) {
|
if (isServicesPortal.value) {
|
||||||
appApi.list().then(res => { portalApps.value = res.data.data })
|
appApi.list().then(res => { portalApps.value = res.data.data })
|
||||||
if (!appKey) return
|
if (!appKey.value) return
|
||||||
|
checkServiceEnabled()
|
||||||
}
|
}
|
||||||
loadApp()
|
loadApp()
|
||||||
loadAppVersions()
|
loadAppVersions()
|
||||||
@ -2132,7 +2194,7 @@ onMounted(() => {
|
|||||||
loadStoreConfigs()
|
loadStoreConfigs()
|
||||||
loadPublishConfig()
|
loadPublishConfig()
|
||||||
loadOperationLogs()
|
loadOperationLogs()
|
||||||
void connectStoreReviewRealtime(appKey, (event: StoreReviewRefreshEvent) => {
|
void connectStoreReviewRealtime(appKey.value, (event: StoreReviewRefreshEvent) => {
|
||||||
const storeName = storeLabel(event.storeType || '') || event.storeType || '应用市场'
|
const storeName = storeLabel(event.storeType || '') || event.storeType || '应用市场'
|
||||||
const stateName = reviewLabel((event.reviewState || '').toUpperCase()) || event.reviewState || '状态变更'
|
const stateName = reviewLabel((event.reviewState || '').toUpperCase()) || event.reviewState || '状态变更'
|
||||||
|
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户