feat(platform): support signed sdk config v2
这个提交包含在:
父节点
209951b16b
当前提交
985c7f78d7
@ -1,6 +1,14 @@
|
|||||||
pipeline {
|
pipeline {
|
||||||
agent any
|
agent any
|
||||||
|
|
||||||
|
parameters {
|
||||||
|
choice(
|
||||||
|
name: 'DEPLOY_ENV',
|
||||||
|
choices: ['development', 'production'],
|
||||||
|
description: 'development 只执行验证;production 需要人工确认后发布'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
environment {
|
environment {
|
||||||
ACR_REGISTRY = 'crpi-n44qjpuucgjt8e8c.cn-beijing.personal.cr.aliyuncs.com'
|
ACR_REGISTRY = 'crpi-n44qjpuucgjt8e8c.cn-beijing.personal.cr.aliyuncs.com'
|
||||||
ACR_NAMESPACE = 'xuqmgroup'
|
ACR_NAMESPACE = 'xuqmgroup'
|
||||||
@ -30,12 +38,33 @@ pipeline {
|
|||||||
$class: 'GitSCM',
|
$class: 'GitSCM',
|
||||||
branches: [[name: 'main']],
|
branches: [[name: 'main']],
|
||||||
extensions: [[$class: 'CleanBeforeCheckout']],
|
extensions: [[$class: 'CleanBeforeCheckout']],
|
||||||
userRemoteConfigs: scm.userRemoteConfigs
|
userRemoteConfigs: [[
|
||||||
|
url: 'https://xuqinmin.com/xuqmGroup/XuqmGroup-Web.git',
|
||||||
|
credentialsId: 'XUQM_GIT_CREDENTIALS'
|
||||||
|
]]
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stage('Verify') {
|
||||||
|
steps {
|
||||||
|
bat 'yarn install --frozen-lockfile && yarn build:tenant'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
stage('Confirm Production') {
|
||||||
|
when { expression { params.DEPLOY_ENV == 'production' } }
|
||||||
|
input {
|
||||||
|
message '确认发布并部署租户平台到生产环境?'
|
||||||
|
ok '确认生产发布'
|
||||||
|
}
|
||||||
|
steps {
|
||||||
|
echo '生产发布已人工确认'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
stage('Resolve Version') {
|
stage('Resolve Version') {
|
||||||
|
when { expression { params.DEPLOY_ENV == 'production' } }
|
||||||
steps {
|
steps {
|
||||||
script {
|
script {
|
||||||
def current = fileExists(env.VERSION_FILE) ? readFile(env.VERSION_FILE).trim() : '1.0.0'
|
def current = fileExists(env.VERSION_FILE) ? readFile(env.VERSION_FILE).trim() : '1.0.0'
|
||||||
@ -50,6 +79,7 @@ pipeline {
|
|||||||
}
|
}
|
||||||
|
|
||||||
stage('Docker Build & Push') {
|
stage('Docker Build & Push') {
|
||||||
|
when { expression { params.DEPLOY_ENV == 'production' } }
|
||||||
steps {
|
steps {
|
||||||
withCredentials([string(credentialsId: 'ACR_PASSWORD', variable: 'ACR_PASS')]) {
|
withCredentials([string(credentialsId: 'ACR_PASSWORD', variable: 'ACR_PASS')]) {
|
||||||
script {
|
script {
|
||||||
@ -76,6 +106,7 @@ pipeline {
|
|||||||
}
|
}
|
||||||
|
|
||||||
stage('Deploy to Production') {
|
stage('Deploy to Production') {
|
||||||
|
when { expression { params.DEPLOY_ENV == 'production' } }
|
||||||
steps {
|
steps {
|
||||||
lock('prod-deploy') {
|
lock('prod-deploy') {
|
||||||
withCredentials([sshUserPrivateKey(credentialsId: 'PROD_SSH_KEY', keyFileVariable: 'SSH_KEY')]) {
|
withCredentials([sshUserPrivateKey(credentialsId: 'PROD_SSH_KEY', keyFileVariable: 'SSH_KEY')]) {
|
||||||
@ -108,6 +139,7 @@ print('versions.json updated: ${env.DEPLOY_SERVICE} = ${env.IMAGE_VERSION}')
|
|||||||
}
|
}
|
||||||
|
|
||||||
stage('Commit Version') {
|
stage('Commit Version') {
|
||||||
|
when { expression { params.DEPLOY_ENV == 'production' } }
|
||||||
steps {
|
steps {
|
||||||
script {
|
script {
|
||||||
bat """
|
bat """
|
||||||
@ -123,7 +155,14 @@ print('versions.json updated: ${env.DEPLOY_SERVICE} = ${env.IMAGE_VERSION}')
|
|||||||
}
|
}
|
||||||
|
|
||||||
post {
|
post {
|
||||||
success { bat "chcp 65001 >nul && echo ✅ ${env.IMAGE_NAME}:${env.IMAGE_VERSION} 构建部署成功" }
|
success {
|
||||||
|
script {
|
||||||
|
def resultText = params.DEPLOY_ENV == 'production'
|
||||||
|
? "${env.IMAGE_NAME}:${env.IMAGE_VERSION} 构建部署成功"
|
||||||
|
: 'tenant-web development 验证成功(未发布)'
|
||||||
|
bat "chcp 65001 >nul && echo ✅ ${resultText}"
|
||||||
|
}
|
||||||
|
}
|
||||||
failure { bat 'chcp 65001 >nul && echo ❌ 构建失败,请检查日志' }
|
failure { bat 'chcp 65001 >nul && echo ❌ 构建失败,请检查日志' }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,7 +5,6 @@ export default defineConfig({
|
|||||||
description: 'XuqmGroup 多端 SDK 接入指南',
|
description: 'XuqmGroup 多端 SDK 接入指南',
|
||||||
base: '/docs/',
|
base: '/docs/',
|
||||||
lang: 'zh-CN',
|
lang: 'zh-CN',
|
||||||
ignoreDeadLinks: true,
|
|
||||||
|
|
||||||
themeConfig: {
|
themeConfig: {
|
||||||
logo: '/logo.svg',
|
logo: '/logo.svg',
|
||||||
@ -42,7 +41,6 @@ export default defineConfig({
|
|||||||
{ text: 'IM 接入', link: '/android/im' },
|
{ text: 'IM 接入', link: '/android/im' },
|
||||||
{ text: '推送接入', link: '/android/push' },
|
{ text: '推送接入', link: '/android/push' },
|
||||||
{ text: '版本管理', link: '/android/update' },
|
{ text: '版本管理', link: '/android/update' },
|
||||||
{ text: '授权管理', link: '/android/license' },
|
|
||||||
],
|
],
|
||||||
'/ios/': [
|
'/ios/': [
|
||||||
{ text: '概览', link: '/ios/' },
|
{ text: '概览', link: '/ios/' },
|
||||||
@ -50,7 +48,6 @@ 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/' },
|
||||||
@ -59,7 +56,6 @@ export default defineConfig({
|
|||||||
{ text: '群聊', link: '/rn/group' },
|
{ text: '群聊', link: '/rn/group' },
|
||||||
{ text: '推送接入', link: '/rn/push' },
|
{ text: '推送接入', link: '/rn/push' },
|
||||||
{ text: '版本管理', link: '/rn/update' },
|
{ text: '版本管理', link: '/rn/update' },
|
||||||
{ text: '授权管理', link: '/rn/license' },
|
|
||||||
],
|
],
|
||||||
'/vue3/': [
|
'/vue3/': [
|
||||||
{ text: '概览', link: '/vue3/' },
|
{ text: '概览', link: '/vue3/' },
|
||||||
@ -72,7 +68,6 @@ export default defineConfig({
|
|||||||
{ text: 'IM 接入', link: '/flutter/im' },
|
{ text: 'IM 接入', link: '/flutter/im' },
|
||||||
{ text: '推送接入', link: '/flutter/push' },
|
{ text: '推送接入', link: '/flutter/push' },
|
||||||
{ text: '版本管理', link: '/flutter/update' },
|
{ text: '版本管理', link: '/flutter/update' },
|
||||||
{ text: '授权管理', link: '/flutter/license' },
|
|
||||||
],
|
],
|
||||||
'/harmony/': [
|
'/harmony/': [
|
||||||
{ text: '概览', link: '/harmony/' },
|
{ text: '概览', link: '/harmony/' },
|
||||||
@ -80,7 +75,6 @@ export default defineConfig({
|
|||||||
{ text: 'IM 接入', link: '/harmony/im' },
|
{ text: 'IM 接入', link: '/harmony/im' },
|
||||||
{ text: '推送接入', link: '/harmony/#push-接入' },
|
{ text: '推送接入', link: '/harmony/#push-接入' },
|
||||||
{ text: '版本管理', link: '/harmony/#update-接入' },
|
{ text: '版本管理', link: '/harmony/#update-接入' },
|
||||||
{ text: '授权管理', link: '/harmony/license' },
|
|
||||||
],
|
],
|
||||||
'/miniprogram/': [
|
'/miniprogram/': [
|
||||||
{ text: '概览', link: '/miniprogram/' },
|
{ text: '概览', link: '/miniprogram/' },
|
||||||
|
|||||||
@ -10,7 +10,6 @@
|
|||||||
| 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` | 设备授权注册与验证 |
|
|
||||||
|
|
||||||
## 快速接入
|
## 快速接入
|
||||||
|
|
||||||
@ -33,7 +32,6 @@ dependencies {
|
|||||||
implementation("com.xuqm:sdk-im:0.4.10")
|
implementation("com.xuqm:sdk-im:0.4.10")
|
||||||
implementation("com.xuqm:sdk-push:0.4.10") // 按需
|
implementation("com.xuqm:sdk-push:0.4.10") // 按需
|
||||||
implementation("com.xuqm:sdk-update:0.4.10") // 按需
|
implementation("com.xuqm:sdk-update:0.4.10") // 按需
|
||||||
implementation("com.xuqm:sdk-license:0.4.10") // 按需
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -199,4 +197,3 @@ lifecycleScope.launch {
|
|||||||
- [Android IM 接入 →](./im)
|
- [Android IM 接入 →](./im)
|
||||||
- [Android 推送接入 →](./push)
|
- [Android 推送接入 →](./push)
|
||||||
- [Android 版本更新 →](./update)
|
- [Android 版本更新 →](./update)
|
||||||
- [Android 授权管理 →](./license)
|
|
||||||
|
|||||||
@ -1,164 +0,0 @@
|
|||||||
# 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` 会自动恢复正常。
|
|
||||||
@ -28,7 +28,6 @@ dependencyResolutionManagement {
|
|||||||
| 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 更新检查、下载安装(需配置 API Key,设置 `serviceActivated = true`)|
|
| sdk-update | `com.xuqm:sdk-update` | App 更新检查、下载安装(需配置 API Key,设置 `serviceActivated = true`)|
|
||||||
| sdk-license | `com.xuqm:sdk-license` | 设备授权注册与验证 |
|
|
||||||
| sdk-bugcollect | `com.xuqm:sdk-bugcollect` | 崩溃日志、ANR、异常上报 |
|
| sdk-bugcollect | `com.xuqm:sdk-bugcollect` | 崩溃日志、ANR、异常上报 |
|
||||||
|
|
||||||
在 `app/build.gradle.kts` 中按需引入:
|
在 `app/build.gradle.kts` 中按需引入:
|
||||||
@ -39,7 +38,6 @@ dependencies {
|
|||||||
implementation("com.xuqm:sdk-im:0.4.10")
|
implementation("com.xuqm:sdk-im:0.4.10")
|
||||||
implementation("com.xuqm:sdk-push:0.4.10") // 按需
|
implementation("com.xuqm:sdk-push:0.4.10") // 按需
|
||||||
implementation("com.xuqm:sdk-update:0.4.10") // 按需
|
implementation("com.xuqm:sdk-update:0.4.10") // 按需
|
||||||
implementation("com.xuqm:sdk-license:0.4.10") // 按需
|
|
||||||
implementation("com.xuqm:sdk-bugcollect:0.4.10") // 按需
|
implementation("com.xuqm:sdk-bugcollect:0.4.10") // 按需
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -98,9 +96,6 @@ dependencies {
|
|||||||
# XuqmSDK Update
|
# XuqmSDK Update
|
||||||
-keep class com.xuqm.sdk.update.** { *; }
|
-keep class com.xuqm.sdk.update.** { *; }
|
||||||
|
|
||||||
# XuqmSDK License
|
|
||||||
-keep class com.xuqm.sdk.license.** { *; }
|
|
||||||
|
|
||||||
# Gson(IM 消息序列化使用)
|
# Gson(IM 消息序列化使用)
|
||||||
-keepattributes Signature
|
-keepattributes Signature
|
||||||
-keepattributes *Annotation*
|
-keepattributes *Annotation*
|
||||||
@ -114,4 +109,3 @@ dependencies {
|
|||||||
- [Android IM 接入 →](./im)
|
- [Android IM 接入 →](./im)
|
||||||
- [Android Push 接入 →](./push)
|
- [Android Push 接入 →](./push)
|
||||||
- [Android 版本更新 →](./update)
|
- [Android 版本更新 →](./update)
|
||||||
- [Android 授权管理 →](./license)
|
|
||||||
|
|||||||
@ -14,7 +14,6 @@
|
|||||||
| `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)|
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -1,197 +0,0 @@
|
|||||||
# 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/xuqmGroup/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_flutter_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 说明
|
|
||||||
|
|
||||||
### 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,设备数量统一计算。
|
|
||||||
@ -28,7 +28,6 @@ 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)|
|
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@ -83,4 +82,3 @@ await XuqmImSdk().logout();
|
|||||||
- [Flutter IM 接入 →](./im)
|
- [Flutter IM 接入 →](./im)
|
||||||
- [Flutter 推送接入 →](./push)
|
- [Flutter 推送接入 →](./push)
|
||||||
- [Flutter 版本更新 →](./update)
|
- [Flutter 版本更新 →](./update)
|
||||||
- [Flutter 授权管理 →](./license)
|
|
||||||
|
|||||||
@ -52,7 +52,6 @@ XuqmGroup 平台由以下核心服务模块组成,各模块可独立按需接
|
|||||||
| **IM** | 即时通讯:单聊、群聊、消息收发、会话管理、好友、群组 |
|
| **IM** | 即时通讯:单聊、群聊、消息收发、会话管理、好友、群组 |
|
||||||
| **Push** | 消息推送:自动检测厂商通道(华为/小米/OPPO/vivo/荣耀/FCM),设备 Token 注册 |
|
| **Push** | 消息推送:自动检测厂商通道(华为/小米/OPPO/vivo/荣耀/FCM),设备 Token 注册 |
|
||||||
| **Update** | 版本更新:App 更新检查、下载安装、商店跳转 |
|
| **Update** | 版本更新:App 更新检查、下载安装、商店跳转 |
|
||||||
| **License** | 授权管理:设备授权注册与验证 |
|
|
||||||
| **BugCollect** | 异常采集:崩溃日志、ANR、异常上报,支持 Android / iOS / RN 多端统一采集 |
|
| **BugCollect** | 异常采集:崩溃日志、ANR、异常上报,支持 Android / iOS / RN 多端统一采集 |
|
||||||
| **File** | 文件服务:文件上传、下载、消息附件管理 |
|
| **File** | 文件服务:文件上传、下载、消息附件管理 |
|
||||||
|
|
||||||
|
|||||||
@ -22,7 +22,7 @@
|
|||||||
6. 客户端集成私有化 SDK。
|
6. 客户端集成私有化 SDK。
|
||||||
7. 客户端使用 `xuqm-private-sdk.json` 初始化。
|
7. 客户端使用 `xuqm-private-sdk.json` 初始化。
|
||||||
8. 业务服务端签发 `UserSig`。
|
8. 业务服务端签发 `UserSig`。
|
||||||
9. 客户端登录 SDK 并使用 IM、Push、Update、File、License、BugCollect 能力。
|
9. 客户端登录 SDK 并使用 IM、Push、Update、File、BugCollect 能力。
|
||||||
|
|
||||||
## 服务端签发 UserSig
|
## 服务端签发 UserSig
|
||||||
|
|
||||||
@ -43,7 +43,7 @@
|
|||||||
-> 获取 userSig
|
-> 获取 userSig
|
||||||
-> XuqmSDK.login(userId, userSig)
|
-> XuqmSDK.login(userId, userSig)
|
||||||
-> IM WebSocket 连接
|
-> IM WebSocket 连接
|
||||||
-> Push / Update / License / BugCollect 模块按需工作
|
-> Push / Update / BugCollect 模块按需工作
|
||||||
```
|
```
|
||||||
|
|
||||||
## 私有化注意事项
|
## 私有化注意事项
|
||||||
|
|||||||
@ -98,8 +98,7 @@ JSON 内容由私有化部署仓库生成,示例:
|
|||||||
"imWsUrl": "wss://im.customer.com/ws/im",
|
"imWsUrl": "wss://im.customer.com/ws/im",
|
||||||
"pushBaseUrl": "https://push.customer.com",
|
"pushBaseUrl": "https://push.customer.com",
|
||||||
"updateBaseUrl": "https://update.customer.com",
|
"updateBaseUrl": "https://update.customer.com",
|
||||||
"fileBaseUrl": "https://file.customer.com",
|
"fileBaseUrl": "https://file.customer.com"
|
||||||
"licenseBaseUrl": "https://license.customer.com"
|
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -111,7 +110,7 @@ JSON 内容由私有化部署仓库生成,示例:
|
|||||||
-> 业务服务端签发 UserSig
|
-> 业务服务端签发 UserSig
|
||||||
-> 客户端调用 SDK login
|
-> 客户端调用 SDK login
|
||||||
-> IM 连接建立
|
-> IM 连接建立
|
||||||
-> 收发消息、推送、更新、License 能力按模块启用
|
-> 收发消息、推送、更新能力按模块启用
|
||||||
```
|
```
|
||||||
|
|
||||||
安全要求:`appSecret` 只能保存在业务服务端,不允许下发到客户端。
|
安全要求:`appSecret` 只能保存在业务服务端,不允许下发到客户端。
|
||||||
|
|||||||
@ -10,7 +10,6 @@
|
|||||||
| `ImSDK` | 单聊、群聊、消息收发 |
|
| `ImSDK` | 单聊、群聊、消息收发 |
|
||||||
| `PushSDK` | Push Token 注册与注销 |
|
| `PushSDK` | Push Token 注册与注销 |
|
||||||
| `UpdateSDK` | App 版本检查、整包更新 |
|
| `UpdateSDK` | App 版本检查、整包更新 |
|
||||||
| `LicenseSDK` | 设备授权注册与验证 |
|
|
||||||
|
|
||||||
## 安装
|
## 安装
|
||||||
|
|
||||||
|
|||||||
@ -1,166 +0,0 @@
|
|||||||
# HarmonyOS 授权管理(License SDK)
|
|
||||||
|
|
||||||
**模块**:`LicenseSDK` · **包名**:`@xuqm/harmony-sdk`
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 1. 安装
|
|
||||||
|
|
||||||
`LicenseSDK` 已包含在 `@xuqm/harmony-sdk` 中,无需额外安装。确保 `oh-package.json5` 中已依赖:
|
|
||||||
|
|
||||||
```json5
|
|
||||||
{
|
|
||||||
"dependencies": {
|
|
||||||
"@xuqm/harmony-sdk": "^0.1.0"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
执行:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
ohpm install
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 2. 放置 License 文件
|
|
||||||
|
|
||||||
从租户平台下载 `.xuqmlicense` 加密文件,放入项目 `resources/rawfile/` 目录:
|
|
||||||
|
|
||||||
```
|
|
||||||
entry/
|
|
||||||
src/main/
|
|
||||||
resources/
|
|
||||||
rawfile/
|
|
||||||
license.xuqm
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 3. 初始化并检查授权
|
|
||||||
|
|
||||||
在 `EntryAbility.onCreate` 或应用启动入口中初始化 License:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { LicenseSDK } from '@xuqm/harmony-sdk'
|
|
||||||
import common from '@ohos.app.ability.common'
|
|
||||||
import rawfileManager from '@ohos.rawfileManager'
|
|
||||||
|
|
||||||
export default class EntryAbility extends UIAbility {
|
|
||||||
async onCreate(want: Want, launchParam: AbilityConstant.LaunchParam) {
|
|
||||||
// 读取 License 文件内容
|
|
||||||
const context = getContext(this) as common.UIAbilityContext
|
|
||||||
const content = await readRawFile(context, 'license.xuqm')
|
|
||||||
|
|
||||||
// 从加密文件初始化
|
|
||||||
await LicenseSDK.initializeFromFile(content)
|
|
||||||
|
|
||||||
// 检查授权
|
|
||||||
const result = await LicenseSDK.checkLicense()
|
|
||||||
if (result.type === 'success') {
|
|
||||||
console.log('授权通过:', result.reason)
|
|
||||||
} else {
|
|
||||||
console.warn('授权失败:', result.message)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function readRawFile(context: common.UIAbilityContext, filename: string): Promise<string> {
|
|
||||||
const rm = context.resourceManager
|
|
||||||
const data = await rm.getRawFileContent(filename)
|
|
||||||
return new TextDecoder().decode(data)
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 4. 携带用户信息
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { LicenseSDK, LicenseUserInfo } from '@xuqm/harmony-sdk'
|
|
||||||
|
|
||||||
const userInfo: LicenseUserInfo = {
|
|
||||||
userId: 'user_001',
|
|
||||||
name: '张三',
|
|
||||||
email: 'zhangsan@company.com',
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await LicenseSDK.checkLicense(userInfo)
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 5. API 说明
|
|
||||||
|
|
||||||
### initializeFromFile
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
LicenseSDK.initializeFromFile(encryptedContent: string): Promise<void>
|
|
||||||
```
|
|
||||||
|
|
||||||
从加密 License 文件内容自动解析 `appKey` 并初始化。
|
|
||||||
|
|
||||||
### checkLicense
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
LicenseSDK.checkLicense(userInfo?: LicenseUserInfo): Promise<LicenseResult>
|
|
||||||
```
|
|
||||||
|
|
||||||
返回 `{ type: 'success', reason: string }` 或 `{ type: 'error', message: string }`。
|
|
||||||
|
|
||||||
**缓存策略**:10 分钟有效期,有效期内不发起网络请求。
|
|
||||||
|
|
||||||
### getStatus
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
LicenseSDK.getStatus(): Promise<'ok' | 'denied' | 'unknown'>
|
|
||||||
```
|
|
||||||
|
|
||||||
### getDeviceId
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
LicenseSDK.getDeviceId(): Promise<string>
|
|
||||||
```
|
|
||||||
|
|
||||||
### clear
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
LicenseSDK.clear(): Promise<void>
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 6. 数据存储
|
|
||||||
|
|
||||||
| 数据 | 存储方式 |
|
|
||||||
|------|---------|
|
|
||||||
| deviceId | `preferences`(应用持久化存储)|
|
|
||||||
| token | `preferences` |
|
|
||||||
| 授权状态 | `preferences` |
|
|
||||||
| statusTime | `preferences` |
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 7. 离线模式
|
|
||||||
|
|
||||||
- 首次激活需要网络连接
|
|
||||||
- 激活后 10 分钟缓存内可离线使用
|
|
||||||
- 网络异常时,若历史缓存成功,继续返回授权通过
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 8. 权限配置
|
|
||||||
|
|
||||||
使用 License SDK 需要网络权限,确保 `module.json5` 中已声明:
|
|
||||||
|
|
||||||
```json5
|
|
||||||
{
|
|
||||||
"module": {
|
|
||||||
"requestPermissions": [
|
|
||||||
{ "name": "ohos.permission.INTERNET" },
|
|
||||||
{ "name": "ohos.permission.GET_NETWORK_INFO" }
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@ -111,4 +111,3 @@ const session = await XuqmSDK.login('user_001', 'your_user_sig_jwt')
|
|||||||
## 下一步
|
## 下一步
|
||||||
|
|
||||||
- [HarmonyOS IM 接入 →](./im)
|
- [HarmonyOS IM 接入 →](./im)
|
||||||
- [HarmonyOS 授权管理 →](./license)
|
|
||||||
|
|||||||
@ -1,190 +0,0 @@
|
|||||||
# 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,设备数量统一计算。
|
|
||||||
@ -111,4 +111,3 @@ XuqmSDK.shared.initialize(config: config)
|
|||||||
- [iOS IM 接入 →](./im)
|
- [iOS IM 接入 →](./im)
|
||||||
- [iOS Push 接入 →](./push)
|
- [iOS Push 接入 →](./push)
|
||||||
- [iOS 版本更新 →](./update)
|
- [iOS 版本更新 →](./update)
|
||||||
- [iOS 授权管理 →](./license)
|
|
||||||
|
|||||||
@ -14,7 +14,6 @@
|
|||||||
| `@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 自动安装,不建议业务方直接引用 |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@ -1,170 +0,0 @@
|
|||||||
# 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` 加密文件,通过 `require()` 将其作为文本资源嵌入 App。
|
|
||||||
|
|
||||||
```ts
|
|
||||||
// 将 license.xuqm 放入 src/assets/
|
|
||||||
const licenseContent = require('./assets/license.xuqm')
|
|
||||||
```
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## 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 说明
|
|
||||||
|
|
||||||
### 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'],
|
|
||||||
},
|
|
||||||
}
|
|
||||||
```
|
|
||||||
@ -97,7 +97,6 @@ allprojects {
|
|||||||
├── @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)
|
|
||||||
└── @xuqm/rn-bugcollect ← 异常采集模块(崩溃、ANR 上报)
|
└── @xuqm/rn-bugcollect ← 异常采集模块(崩溃、ANR 上报)
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -146,4 +145,3 @@ yarn add @xuqm/rn-bugcollect
|
|||||||
- [RN 群聊 →](./group)
|
- [RN 群聊 →](./group)
|
||||||
- [RN 推送接入 →](./push)
|
- [RN 推送接入 →](./push)
|
||||||
- [RN 版本更新 →](./update)
|
- [RN 版本更新 →](./update)
|
||||||
- [RN 授权管理 →](./license)
|
|
||||||
|
|||||||
26
docs/SDK_PLATFORM_V2_HANDOFF.md
普通文件
26
docs/SDK_PLATFORM_V2_HANDOFF.md
普通文件
@ -0,0 +1,26 @@
|
|||||||
|
# 租户平台 SDK V2 页面接手说明
|
||||||
|
|
||||||
|
SDK 平台的完整服务端契约见 Server 仓库 `docs/SDK_PLATFORM_V2_HANDOFF.md`。
|
||||||
|
|
||||||
|
当前 Web 变更:
|
||||||
|
|
||||||
|
- 应用详情展示 Config 的配置 ID、版本、签发时间和到期时间。
|
||||||
|
- 重新生成 Config 时必须明确选择长期有效或指定到期时间;指定时间必须晚于当前时间,并以 ISO 8601 UTC 传给服务端。
|
||||||
|
- BugCollect 页面统一展示 `RN_SOURCEMAP` 和 `R8_MAPPING`,手动上传示例使用 `/bugcollect/v1/artifacts/upload`。
|
||||||
|
- 产物上传示例只使用 `Authorization: Bearer XUQM_API_TOKEN`,不得把 Token 放入 URL。
|
||||||
|
- 已删除废弃的离线 license 服务页面、路由、API、菜单和日志入口。
|
||||||
|
- 依赖管理统一为根 `package.json` 声明的 Yarn 1 与 `yarn.lock`。
|
||||||
|
- `Jenkinsfile.tenant-web` 默认 development,仅验证;production 需要人工确认才允许推送、部署和提交版本号。
|
||||||
|
|
||||||
|
验证:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
yarn install --frozen-lockfile
|
||||||
|
yarn build:tenant
|
||||||
|
yarn build:ops
|
||||||
|
yarn workspace @xuqm/docs-site build
|
||||||
|
```
|
||||||
|
|
||||||
|
2026-07-26 三项构建均已通过。
|
||||||
|
|
||||||
|
未执行提交、推送、Jenkins 或部署。
|
||||||
2
ops-platform/components.d.ts
vendored
2
ops-platform/components.d.ts
vendored
@ -13,7 +13,6 @@ declare module 'vue' {
|
|||||||
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
|
||||||
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']
|
||||||
@ -31,6 +30,7 @@ declare module 'vue' {
|
|||||||
ElOption: typeof import('element-plus/es')['ElOption']
|
ElOption: typeof import('element-plus/es')['ElOption']
|
||||||
ElPageHeader: typeof import('element-plus/es')['ElPageHeader']
|
ElPageHeader: typeof import('element-plus/es')['ElPageHeader']
|
||||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||||
|
ElPopover: typeof import('element-plus/es')['ElPopover']
|
||||||
ElRow: typeof import('element-plus/es')['ElRow']
|
ElRow: typeof import('element-plus/es')['ElRow']
|
||||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||||
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
|
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
|
||||||
|
|||||||
@ -58,11 +58,6 @@
|
|||||||
|
|
||||||
<el-dialog v-model="showReview" :title="reviewAction === 'approve' ? '通过申请' : '拒绝申请'" width="420px">
|
<el-dialog v-model="showReview" :title="reviewAction === 'approve' ? '通过申请' : '拒绝申请'" width="420px">
|
||||||
<el-form>
|
<el-form>
|
||||||
<el-form-item v-if="reviewAction === 'approve' && currentRequest?.serviceType === 'LICENSE'" label="有效期">
|
|
||||||
<el-date-picker v-model="expiresAt" type="datetime" placeholder="选择过期时间(可选,留空为永久)"
|
|
||||||
style="width:100%" value-format="YYYY-MM-DDTHH:mm:ss" :disabled-date="(date: Date) => date.getTime() < Date.now() - 86400000" />
|
|
||||||
<div class="el-text--info" style="font-size:12px;margin-top:4px">设置后不可修改,留空表示永久有效</div>
|
|
||||||
</el-form-item>
|
|
||||||
<el-form-item label="审核备注">
|
<el-form-item label="审核备注">
|
||||||
<el-input v-model="reviewNote" type="textarea" :rows="3" placeholder="可选备注" />
|
<el-input v-model="reviewNote" type="textarea" :rows="3" placeholder="可选备注" />
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
@ -92,7 +87,6 @@ const total = ref(0)
|
|||||||
const showReview = ref(false)
|
const showReview = ref(false)
|
||||||
const reviewAction = ref<'approve' | 'reject'>('approve')
|
const reviewAction = ref<'approve' | 'reject'>('approve')
|
||||||
const reviewNote = ref('')
|
const reviewNote = ref('')
|
||||||
const expiresAt = ref('')
|
|
||||||
const currentRequest = ref<ServiceRequest | null>(null)
|
const currentRequest = ref<ServiceRequest | null>(null)
|
||||||
const submitting = ref(false)
|
const submitting = ref(false)
|
||||||
|
|
||||||
@ -113,7 +107,6 @@ function openReview(row: ServiceRequest, action: 'approve' | 'reject') {
|
|||||||
currentRequest.value = row
|
currentRequest.value = row
|
||||||
reviewAction.value = action
|
reviewAction.value = action
|
||||||
reviewNote.value = ''
|
reviewNote.value = ''
|
||||||
expiresAt.value = ''
|
|
||||||
showReview.value = true
|
showReview.value = true
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -122,8 +115,7 @@ async function submitReview() {
|
|||||||
submitting.value = true
|
submitting.value = true
|
||||||
try {
|
try {
|
||||||
if (reviewAction.value === 'approve') {
|
if (reviewAction.value === 'approve') {
|
||||||
const exp = currentRequest.value.serviceType === 'LICENSE' ? expiresAt.value || undefined : undefined
|
await opsApi.approveRequest(currentRequest.value.id, reviewNote.value)
|
||||||
await opsApi.approveRequest(currentRequest.value.id, reviewNote.value, exp)
|
|
||||||
ElMessage.success('已通过申请,服务已开通')
|
ElMessage.success('已通过申请,服务已开通')
|
||||||
} else {
|
} else {
|
||||||
await opsApi.rejectRequest(currentRequest.value.id, reviewNote.value)
|
await opsApi.rejectRequest(currentRequest.value.id, reviewNote.value)
|
||||||
|
|||||||
4746
package-lock.json
自动生成的
4746
package-lock.json
自动生成的
文件差异内容过多而无法显示
加载差异
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
## 项目定位
|
## 项目定位
|
||||||
|
|
||||||
XuqmGroup 租户管理平台 Web 前端。Vue3 + TypeScript + Element Plus,管理应用、IM、推送、版本、License 等功能。
|
XuqmGroup 租户管理平台 Web 前端。Vue3 + TypeScript + Element Plus,管理应用、IM、推送、版本和崩溃收集等功能。
|
||||||
|
|
||||||
- Git 远端:`https://xuqinmin.com/xuqmGroup/XuqmGroup-Web.git`(子目录 `tenant-platform`)
|
- Git 远端:`https://xuqinmin.com/xuqmGroup/XuqmGroup-Web.git`(子目录 `tenant-platform`)
|
||||||
- 技术栈:Vue 3.5 + TypeScript 5.8 + Vite 6 + Element Plus 2.9 + Pinia 3 + Vue Router 4
|
- 技术栈:Vue 3.5 + TypeScript 5.8 + Vite 6 + Element Plus 2.9 + Pinia 3 + Vue Router 4
|
||||||
@ -35,7 +35,6 @@ tenant-platform/
|
|||||||
| `views/im/` | IM 管理(配置 / Webhook / 告警) |
|
| `views/im/` | IM 管理(配置 / Webhook / 告警) |
|
||||||
| `views/push/` | 推送管理(配置 / 设备管理) |
|
| `views/push/` | 推送管理(配置 / 设备管理) |
|
||||||
| `views/update/` | 版本管理(APK / RN Bundle) |
|
| `views/update/` | 版本管理(APK / RN Bundle) |
|
||||||
| `views/license/` | License 管理 |
|
|
||||||
| `views/logs/` | 操作日志 |
|
| `views/logs/` | 操作日志 |
|
||||||
| `views/accounts/` | 子账号管理 |
|
| `views/accounts/` | 子账号管理 |
|
||||||
| `views/security/` | 安全中心 |
|
| `views/security/` | 安全中心 |
|
||||||
@ -56,7 +55,6 @@ tenant-platform/
|
|||||||
| `/apps/:appKey/im-config` | IM 配置 |
|
| `/apps/:appKey/im-config` | IM 配置 |
|
||||||
| `/apps/:appKey/push-config` | 推送配置 |
|
| `/apps/:appKey/push-config` | 推送配置 |
|
||||||
| `/apps/:appKey/update` | 版本管理 |
|
| `/apps/:appKey/update` | 版本管理 |
|
||||||
| `/apps/:appKey/license` | License 管理 |
|
|
||||||
| `/operation-logs` | 操作日志 |
|
| `/operation-logs` | 操作日志 |
|
||||||
| `/accounts` | 子账号 |
|
| `/accounts` | 子账号 |
|
||||||
| `/security` | 安全中心 |
|
| `/security` | 安全中心 |
|
||||||
@ -70,7 +68,6 @@ tenant-platform/
|
|||||||
| `api/im.ts` | im-service |
|
| `api/im.ts` | im-service |
|
||||||
| `api/push.ts` | push-service |
|
| `api/push.ts` | push-service |
|
||||||
| `api/update.ts` | update-service |
|
| `api/update.ts` | update-service |
|
||||||
| `api/license.ts` | license-service |
|
|
||||||
| `api/account.ts` | tenant-service(子账号) |
|
| `api/account.ts` | tenant-service(子账号) |
|
||||||
| `api/dashboard.ts` | tenant-service(统计) |
|
| `api/dashboard.ts` | tenant-service(统计) |
|
||||||
| `api/system.ts` | tenant-service(系统) |
|
| `api/system.ts` | tenant-service(系统) |
|
||||||
@ -80,7 +77,6 @@ tenant-platform/
|
|||||||
|
|
||||||
```ts
|
```ts
|
||||||
proxy: {
|
proxy: {
|
||||||
'/api/license': { target: 'http://127.0.0.1:8085' },
|
|
||||||
'/api/push': { target: 'http://127.0.0.1:8083' },
|
'/api/push': { target: 'http://127.0.0.1:8083' },
|
||||||
'/api': { target: 'http://127.0.0.1:8081' },
|
'/api': { target: 'http://127.0.0.1:8081' },
|
||||||
}
|
}
|
||||||
|
|||||||
4
tenant-platform/components.d.ts
vendored
4
tenant-platform/components.d.ts
vendored
@ -42,13 +42,15 @@ declare module 'vue' {
|
|||||||
ElOption: typeof import('element-plus/es')['ElOption']
|
ElOption: typeof import('element-plus/es')['ElOption']
|
||||||
ElPageHeader: typeof import('element-plus/es')['ElPageHeader']
|
ElPageHeader: typeof import('element-plus/es')['ElPageHeader']
|
||||||
ElPagination: typeof import('element-plus/es')['ElPagination']
|
ElPagination: typeof import('element-plus/es')['ElPagination']
|
||||||
|
ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm']
|
||||||
|
ElPopover: typeof import('element-plus/es')['ElPopover']
|
||||||
ElProgress: typeof import('element-plus/es')['ElProgress']
|
ElProgress: typeof import('element-plus/es')['ElProgress']
|
||||||
ElRadio: typeof import('element-plus/es')['ElRadio']
|
ElRadio: typeof import('element-plus/es')['ElRadio']
|
||||||
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
ElRadioButton: typeof import('element-plus/es')['ElRadioButton']
|
||||||
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
|
||||||
ElRow: typeof import('element-plus/es')['ElRow']
|
ElRow: typeof import('element-plus/es')['ElRow']
|
||||||
ElSegmented: typeof import('element-plus/es')['ElSegmented']
|
|
||||||
ElSelect: typeof import('element-plus/es')['ElSelect']
|
ElSelect: typeof import('element-plus/es')['ElSelect']
|
||||||
|
ElSkeleton: typeof import('element-plus/es')['ElSkeleton']
|
||||||
ElSlider: typeof import('element-plus/es')['ElSlider']
|
ElSlider: typeof import('element-plus/es')['ElSlider']
|
||||||
ElSpace: typeof import('element-plus/es')['ElSpace']
|
ElSpace: typeof import('element-plus/es')['ElSpace']
|
||||||
ElStatistic: typeof import('element-plus/es')['ElStatistic']
|
ElStatistic: typeof import('element-plus/es')['ElStatistic']
|
||||||
|
|||||||
@ -11,9 +11,21 @@ export interface App {
|
|||||||
iconUrl?: string
|
iconUrl?: string
|
||||||
appKey: string
|
appKey: string
|
||||||
appSecret: string
|
appSecret: string
|
||||||
|
configFileId?: string | null
|
||||||
|
configFileRevision?: number | null
|
||||||
|
configFileIssuedAt?: string | null
|
||||||
|
configFileExpiresAt?: string | null
|
||||||
createdAt: string
|
createdAt: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface ConfigFileMetadata {
|
||||||
|
configId: string
|
||||||
|
revision: number
|
||||||
|
issuedAt: string
|
||||||
|
expiresAt: string | null
|
||||||
|
longTerm: boolean
|
||||||
|
}
|
||||||
|
|
||||||
export interface CreateAppRequest {
|
export interface CreateAppRequest {
|
||||||
packageName?: string
|
packageName?: string
|
||||||
iosBundleId?: string
|
iosBundleId?: string
|
||||||
@ -27,7 +39,7 @@ export interface FeatureService {
|
|||||||
id: string
|
id: string
|
||||||
appKey: string
|
appKey: string
|
||||||
platform: 'ANDROID' | 'IOS' | 'HARMONY'
|
platform: 'ANDROID' | 'IOS' | 'HARMONY'
|
||||||
serviceType: 'IM' | 'PUSH' | 'UPDATE' | 'LICENSE' | 'BUG_COLLECT'
|
serviceType: 'IM' | 'PUSH' | 'UPDATE' | 'BUG_COLLECT'
|
||||||
enabled: boolean
|
enabled: boolean
|
||||||
config?: string | null
|
config?: string | null
|
||||||
createdAt: string
|
createdAt: string
|
||||||
@ -205,16 +217,16 @@ export const appApi = {
|
|||||||
downloadConfigFile: (appKey: string) =>
|
downloadConfigFile: (appKey: string) =>
|
||||||
client.get<Blob>(`/apps/${appKey}/config-file`, { responseType: 'blob' }),
|
client.get<Blob>(`/apps/${appKey}/config-file`, { responseType: 'blob' }),
|
||||||
|
|
||||||
regenerateConfigFile: (appKey: string) =>
|
regenerateConfigFile: (appKey: string, data: { longTerm: boolean; expiresAt: string | null }) =>
|
||||||
client.post<{ data: null }>(`/apps/${appKey}/config-file/regenerate`),
|
client.post<{ data: ConfigFileMetadata }>(`/apps/${appKey}/config-file/regenerate`, data),
|
||||||
|
|
||||||
requestActivation: (appKey: string, serviceType: 'IM' | 'PUSH' | 'UPDATE' | 'LICENSE' | 'BUG_COLLECT', reason: string) =>
|
requestActivation: (appKey: string, serviceType: 'IM' | 'PUSH' | 'UPDATE' | 'BUG_COLLECT', reason: string) =>
|
||||||
client.post<{ data: null }>(`/apps/${appKey}/services/request-activation`, null, {
|
client.post<{ data: null }>(`/apps/${appKey}/services/request-activation`, null, {
|
||||||
params: { platform: 'ANDROID', serviceType, applyReason: reason },
|
params: { platform: 'ANDROID', serviceType, applyReason: reason },
|
||||||
}),
|
}),
|
||||||
|
|
||||||
parseConfigFile: (content: string) =>
|
parseConfigFile: (content: string) =>
|
||||||
client.post<{ data: { appKey: string; appName: string; packageName: string; iosBundleId: string; harmonyBundleName: string; baseUrl: string; serverUrl: string } }>(
|
client.post<{ data: { schemaVersion: number; configId: string; revision: number; issuedAt: string; expiresAt: string | null; appKey: string; appName: string; packageName: string; iosBundleId: string; harmonyBundleName: string; serverUrl: string } }>(
|
||||||
'/apps/config/parse',
|
'/apps/config/parse',
|
||||||
{ content },
|
{ content },
|
||||||
),
|
),
|
||||||
|
|||||||
@ -106,8 +106,14 @@ export interface BugCollectSourcemap {
|
|||||||
appKey: string
|
appKey: string
|
||||||
platform: string
|
platform: string
|
||||||
appVersion: string
|
appVersion: string
|
||||||
bundleName: string
|
|
||||||
buildId: string | null
|
buildId: string | null
|
||||||
|
moduleId: string | null
|
||||||
|
moduleVersion: string | null
|
||||||
|
bundleHash: string | null
|
||||||
|
artifactType: 'RN_SOURCEMAP' | 'R8_MAPPING'
|
||||||
|
artifactHash: string | null
|
||||||
|
contentHash: string
|
||||||
|
storageKey: string
|
||||||
uploadedAt: string
|
uploadedAt: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -1,139 +0,0 @@
|
|||||||
import axios from 'axios'
|
|
||||||
import { ElMessage } from 'element-plus'
|
|
||||||
import router from '@/router'
|
|
||||||
import { isJwtExpired } from '@/utils/jwt'
|
|
||||||
|
|
||||||
const licenseClient = axios.create({
|
|
||||||
baseURL: import.meta.env.VITE_LICENSE_API_BASE_URL ?? '',
|
|
||||||
timeout: 15000,
|
|
||||||
})
|
|
||||||
|
|
||||||
if (import.meta.env.DEV) {
|
|
||||||
licenseClient.interceptors.request.use((config) => {
|
|
||||||
console.debug('[tenant-platform][License] request', {
|
|
||||||
method: config.method?.toUpperCase(),
|
|
||||||
url: config.baseURL ? `${config.baseURL}${config.url ?? ''}` : config.url,
|
|
||||||
params: config.params,
|
|
||||||
})
|
|
||||||
return config
|
|
||||||
})
|
|
||||||
licenseClient.interceptors.response.use((res) => {
|
|
||||||
console.debug('[tenant-platform][License] response', {
|
|
||||||
status: res.status,
|
|
||||||
url: res.config.url,
|
|
||||||
})
|
|
||||||
return res
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
licenseClient.interceptors.request.use((config) => {
|
|
||||||
const token = localStorage.getItem('token')
|
|
||||||
if (token && !isJwtExpired(token)) {
|
|
||||||
config.headers.Authorization = `Bearer ${token}`
|
|
||||||
} else if (token && isJwtExpired(token)) {
|
|
||||||
localStorage.removeItem('token')
|
|
||||||
if (router.currentRoute.value.path !== '/login') {
|
|
||||||
router.push('/login?reason=' + encodeURIComponent('登录已失效,请重新登录'))
|
|
||||||
}
|
|
||||||
return Promise.reject(new Error('登录已失效,请重新登录'))
|
|
||||||
}
|
|
||||||
return config
|
|
||||||
})
|
|
||||||
|
|
||||||
licenseClient.interceptors.response.use(
|
|
||||||
(res) => res,
|
|
||||||
(error) => {
|
|
||||||
const status = error.response?.status
|
|
||||||
if (status === 401) {
|
|
||||||
localStorage.removeItem('token')
|
|
||||||
if (router.currentRoute.value.path !== '/login') {
|
|
||||||
router.push('/login')
|
|
||||||
}
|
|
||||||
ElMessage.error('登录已失效,请重新登录')
|
|
||||||
return Promise.reject(error)
|
|
||||||
}
|
|
||||||
if (status === 403) {
|
|
||||||
localStorage.removeItem('token')
|
|
||||||
if (router.currentRoute.value.path !== '/login') {
|
|
||||||
router.push('/login?reason=' + encodeURIComponent('登录已失效,请重新登录'))
|
|
||||||
}
|
|
||||||
ElMessage.error(error.response?.data?.message ?? '登录已失效,请重新登录')
|
|
||||||
return Promise.reject(error)
|
|
||||||
}
|
|
||||||
const msg = error.response?.data?.message ?? '授权请求失败'
|
|
||||||
ElMessage.error(msg)
|
|
||||||
return Promise.reject(error)
|
|
||||||
},
|
|
||||||
)
|
|
||||||
|
|
||||||
export interface AppLicense {
|
|
||||||
id: string
|
|
||||||
name: string
|
|
||||||
maxDevices: number
|
|
||||||
registeredDevices: number
|
|
||||||
expiresAt?: string | null
|
|
||||||
isActive: boolean
|
|
||||||
remark?: string | null
|
|
||||||
createdAt: string
|
|
||||||
updatedAt: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LicenseDevice {
|
|
||||||
id: string
|
|
||||||
appKey: string
|
|
||||||
deviceId: string
|
|
||||||
deviceName?: string | null
|
|
||||||
deviceModel?: string | null
|
|
||||||
deviceVendor?: string | null
|
|
||||||
osVersion?: string | null
|
|
||||||
userId?: string | null
|
|
||||||
userName?: string | null
|
|
||||||
userEmail?: string | null
|
|
||||||
userPhone?: string | null
|
|
||||||
userInfoJson?: string | null
|
|
||||||
registeredAt: string
|
|
||||||
lastVerifiedAt?: string | null
|
|
||||||
isActive: boolean
|
|
||||||
createdAt: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface LicenseOperationLog {
|
|
||||||
id: string
|
|
||||||
appKey: string
|
|
||||||
operator: string
|
|
||||||
action: string
|
|
||||||
resourceType: string
|
|
||||||
resourceId?: string | null
|
|
||||||
summary?: string | null
|
|
||||||
detailJson?: string | null
|
|
||||||
createdAt: string
|
|
||||||
}
|
|
||||||
|
|
||||||
export const licenseApi = {
|
|
||||||
getAppLicense(appKey: string) {
|
|
||||||
return licenseClient.get<{ data: { license: AppLicense; devices: LicenseDevice[] } }>(
|
|
||||||
`/api/license/admin/apps/${encodeURIComponent(appKey)}`,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
|
|
||||||
updateAppLicense(appKey: string, data: { maxDevices?: number; isActive?: boolean; remark?: string; expiresAt?: string }) {
|
|
||||||
return licenseClient.patch<{ data: AppLicense }>(
|
|
||||||
`/api/license/admin/apps/${encodeURIComponent(appKey)}`,
|
|
||||||
data,
|
|
||||||
)
|
|
||||||
},
|
|
||||||
|
|
||||||
revokeDevice(id: string) {
|
|
||||||
return licenseClient.delete<{ data: null }>(`/api/license/admin/devices/${encodeURIComponent(id)}`)
|
|
||||||
},
|
|
||||||
|
|
||||||
reactivateDevice(id: string) {
|
|
||||||
return licenseClient.put<{ data: null }>(`/api/license/admin/devices/${encodeURIComponent(id)}/reactivate`)
|
|
||||||
},
|
|
||||||
|
|
||||||
getOperationLogs(appKey: string, page = 0, size = 20) {
|
|
||||||
return licenseClient.get<{ data: { content: LicenseOperationLog[]; total: number; totalPages: number } }>(
|
|
||||||
`/api/license/admin/operation-logs`, { params: { appKey, page, size } },
|
|
||||||
)
|
|
||||||
},
|
|
||||||
}
|
|
||||||
1
tenant-platform/src/env.d.ts
vendored
1
tenant-platform/src/env.d.ts
vendored
@ -3,7 +3,6 @@
|
|||||||
interface ImportMetaEnv {
|
interface ImportMetaEnv {
|
||||||
readonly VITE_API_BASE_URL: string
|
readonly VITE_API_BASE_URL: string
|
||||||
readonly VITE_FILE_SERVICE_URL: string
|
readonly VITE_FILE_SERVICE_URL: string
|
||||||
readonly VITE_LICENSE_API_BASE_URL: string
|
|
||||||
readonly BASE_URL: string
|
readonly BASE_URL: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -85,10 +85,6 @@ const router = createRouter({
|
|||||||
path: 'apps/:appKey/update',
|
path: 'apps/:appKey/update',
|
||||||
component: () => import('@/views/update/VersionManagementView.vue'),
|
component: () => import('@/views/update/VersionManagementView.vue'),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'apps/:appKey/license',
|
|
||||||
component: () => import('@/views/license/LicenseManagementView.vue'),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'services/im/:appKey?',
|
path: 'services/im/:appKey?',
|
||||||
component: () => import('@/views/im/ImManagementView.vue'),
|
component: () => import('@/views/im/ImManagementView.vue'),
|
||||||
@ -101,10 +97,6 @@ const router = createRouter({
|
|||||||
path: 'services/update/:appKey?',
|
path: 'services/update/:appKey?',
|
||||||
component: () => import('@/views/update/VersionManagementView.vue'),
|
component: () => import('@/views/update/VersionManagementView.vue'),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'services/license/:appKey?',
|
|
||||||
component: () => import('@/views/license/LicenseManagementView.vue'),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'services/bugcollect/:appKey?',
|
path: 'services/bugcollect/:appKey?',
|
||||||
component: () => import('@/views/bug-collect/BugCollectOverview.vue'),
|
component: () => import('@/views/bug-collect/BugCollectOverview.vue'),
|
||||||
|
|||||||
@ -31,7 +31,12 @@
|
|||||||
<el-descriptions-item label="简述" :span="2">{{ app.description || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="简述" :span="2">{{ app.description || '-' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="Config 文件">
|
<el-descriptions-item label="Config 文件">
|
||||||
<el-button size="small" type="primary" plain @click="downloadConfigFile">下载</el-button>
|
<el-button size="small" type="primary" plain @click="downloadConfigFile">下载</el-button>
|
||||||
<el-button size="small" plain @click="regenerateConfigFile" :loading="regenerating">重新生成</el-button>
|
<el-button size="small" plain @click="openRegenerateDialog">重新生成</el-button>
|
||||||
|
<div class="config-metadata">
|
||||||
|
<span>版本:{{ app.configFileRevision ?? '尚未生成' }}</span>
|
||||||
|
<span>签发:{{ app.configFileIssuedAt ? formatTime(app.configFileIssuedAt) : '-' }}</span>
|
||||||
|
<span>有效期:{{ app.configFileExpiresAt ? formatTime(app.configFileExpiresAt) : '长期有效' }}</span>
|
||||||
|
</div>
|
||||||
</el-descriptions-item>
|
</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
</el-card>
|
</el-card>
|
||||||
@ -138,40 +143,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-card class="info-card" style="margin-top:16px">
|
|
||||||
<template #header>授权管理</template>
|
|
||||||
<div class="service-grid">
|
|
||||||
<el-card class="service-card">
|
|
||||||
<div class="service-header">
|
|
||||||
<div class="service-title-block">
|
|
||||||
<span class="service-name">{{ serviceLabel('LICENSE') }}</span>
|
|
||||||
<span class="service-help">{{ serviceHelp('LICENSE') }}</span>
|
|
||||||
</div>
|
|
||||||
<el-switch
|
|
||||||
:model-value="isServiceEnabled('LICENSE')"
|
|
||||||
@change="(val: boolean) => onToggleService('LICENSE', val)"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div class="service-status-row">
|
|
||||||
<el-tag :type="isServiceEnabled('LICENSE') ? 'success' : 'info'" size="small">
|
|
||||||
{{ isServiceEnabled('LICENSE') ? '已开通' : '未开通' }}
|
|
||||||
</el-tag>
|
|
||||||
<span class="service-status-text">
|
|
||||||
管理 PAD 设备的授权注册与验证。
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<div class="service-actions">
|
|
||||||
<el-button v-if="isServiceEnabled('LICENSE')" size="small" type="primary" plain @click="$router.push(`/apps/${app.appKey}/license`)">
|
|
||||||
授权管理 →
|
|
||||||
</el-button>
|
|
||||||
<el-button v-else size="small" type="primary" plain @click="openActivationRequest('LICENSE')">
|
|
||||||
申请开通
|
|
||||||
</el-button>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<el-card class="info-card" style="margin-top:16px">
|
<el-card class="info-card" style="margin-top:16px">
|
||||||
<template #header>崩溃收集</template>
|
<template #header>崩溃收集</template>
|
||||||
<div class="service-grid">
|
<div class="service-grid">
|
||||||
@ -222,7 +193,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<p style="color:#606266;font-size:13px;margin-bottom:12px">
|
<p style="color:#606266;font-size:13px;margin-bottom:12px">
|
||||||
API Key 用于外部工具(Postman / CI/CD)调用平台 API 进行认证,支持 update / push / im / license 等所有服务。每个应用最多 8 个,创建后仅显示一次。
|
API Key 用于外部工具(Postman / CI/CD)调用平台 API 进行认证,支持 update / push / im / bugcollect 等服务。每个应用最多 8 个,创建后仅显示一次。
|
||||||
</p>
|
</p>
|
||||||
<el-table :data="apiKeys" v-loading="loadingApiKeys" border stripe>
|
<el-table :data="apiKeys" v-loading="loadingApiKeys" border stripe>
|
||||||
<el-table-column prop="name" label="名称" min-width="120" />
|
<el-table-column prop="name" label="名称" min-width="120" />
|
||||||
@ -333,6 +304,43 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog v-model="showRegenerateDialog" title="重新生成 Config 文件" :width="dialogWidth">
|
||||||
|
<el-alert
|
||||||
|
title="重新生成后旧文件将失效;已安装应用仍可运行,但下次构建必须使用新文件。"
|
||||||
|
type="warning"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
style="margin-bottom:16px"
|
||||||
|
/>
|
||||||
|
<el-descriptions :column="1" border style="margin-bottom:16px">
|
||||||
|
<el-descriptions-item label="当前版本">{{ app.configFileRevision ?? '尚未生成' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="配置 ID">{{ app.configFileId || '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="签发时间">{{ app.configFileIssuedAt ? formatTime(app.configFileIssuedAt) : '-' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="到期时间">{{ app.configFileExpiresAt ? formatTime(app.configFileExpiresAt) : '长期有效' }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
<el-form label-width="90px">
|
||||||
|
<el-form-item label="有效方式">
|
||||||
|
<el-radio-group v-model="configExpiryMode">
|
||||||
|
<el-radio value="longTerm">长期有效</el-radio>
|
||||||
|
<el-radio value="expiresAt">指定到期时间</el-radio>
|
||||||
|
</el-radio-group>
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item v-if="configExpiryMode === 'expiresAt'" label="到期时间">
|
||||||
|
<el-date-picker
|
||||||
|
v-model="configExpiresAt"
|
||||||
|
type="datetime"
|
||||||
|
placeholder="请选择到期时间"
|
||||||
|
:disabled-date="date => date.getTime() < Date.now() - 86400000"
|
||||||
|
style="width:100%"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="showRegenerateDialog = false">取消</el-button>
|
||||||
|
<el-button type="primary" :loading="regenerating" @click="regenerateConfigFile">确认重新生成</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
<!-- Activation Request Dialog -->
|
<!-- Activation Request Dialog -->
|
||||||
<el-dialog v-model="showActivationDialog" title="申请开通服务" :width="dialogWidth">
|
<el-dialog v-model="showActivationDialog" title="申请开通服务" :width="dialogWidth">
|
||||||
<el-form label-width="80px">
|
<el-form label-width="80px">
|
||||||
@ -383,6 +391,9 @@ const showActivationDialog = ref(false)
|
|||||||
const submittingActivation = ref(false)
|
const submittingActivation = ref(false)
|
||||||
const activationForm = ref({ platform: '', serviceType: '', reason: '' })
|
const activationForm = ref({ platform: '', serviceType: '', reason: '' })
|
||||||
const regenerating = ref(false)
|
const regenerating = ref(false)
|
||||||
|
const showRegenerateDialog = ref(false)
|
||||||
|
const configExpiryMode = ref<'longTerm' | 'expiresAt'>('longTerm')
|
||||||
|
const configExpiresAt = ref<Date | null>(null)
|
||||||
|
|
||||||
// ── API Key Management ──
|
// ── API Key Management ──
|
||||||
const apiKeys = ref<{ id: string; appKey: string; apiKey: string; name: string; enabled: boolean; createdAt: string }[]>([])
|
const apiKeys = ref<{ id: string; appKey: string; apiKey: string; name: string; enabled: boolean; createdAt: string }[]>([])
|
||||||
@ -527,7 +538,6 @@ function serviceLabel(type: string) {
|
|||||||
IM: '即时通讯 (IM)',
|
IM: '即时通讯 (IM)',
|
||||||
PUSH: '离线推送',
|
PUSH: '离线推送',
|
||||||
UPDATE: '版本管理',
|
UPDATE: '版本管理',
|
||||||
LICENSE: '授权管理',
|
|
||||||
BUG_COLLECT: '崩溃收集 (BugCollect)',
|
BUG_COLLECT: '崩溃收集 (BugCollect)',
|
||||||
}[type] ?? type
|
}[type] ?? type
|
||||||
}
|
}
|
||||||
@ -537,7 +547,6 @@ function serviceHelp(type: string) {
|
|||||||
IM: 'IM 服务独立开通后,在管理页配置回调和消息能力。',
|
IM: 'IM 服务独立开通后,在管理页配置回调和消息能力。',
|
||||||
PUSH: '一次开通后,可在推送配置页按厂商维护配置。',
|
PUSH: '一次开通后,可在推送配置页按厂商维护配置。',
|
||||||
UPDATE: '一次开通后,版本管理页独立管理版本上传、商店配置和灰度发布。',
|
UPDATE: '一次开通后,版本管理页独立管理版本上传、商店配置和灰度发布。',
|
||||||
LICENSE: '管理 PAD 设备的授权注册与验证。',
|
|
||||||
BUG_COLLECT: '采集 App 崩溃与自定义事件,支持漏斗分析和 Webhook 告警。',
|
BUG_COLLECT: '采集 App 崩溃与自定义事件,支持漏斗分析和 Webhook 告警。',
|
||||||
}[type] ?? ''
|
}[type] ?? ''
|
||||||
}
|
}
|
||||||
@ -654,21 +663,35 @@ async function downloadConfigFile() {
|
|||||||
URL.revokeObjectURL(url)
|
URL.revokeObjectURL(url)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openRegenerateDialog() {
|
||||||
|
configExpiryMode.value = app.value?.configFileExpiresAt ? 'expiresAt' : 'longTerm'
|
||||||
|
configExpiresAt.value = app.value?.configFileExpiresAt ? new Date(app.value.configFileExpiresAt) : null
|
||||||
|
showRegenerateDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
async function regenerateConfigFile() {
|
async function regenerateConfigFile() {
|
||||||
const current = app.value
|
const current = app.value
|
||||||
if (!current) return
|
if (!current) return
|
||||||
try {
|
if (configExpiryMode.value === 'expiresAt'
|
||||||
await ElMessageBox.confirm('重新生成后,旧的 Config 文件将失效,需要重新下载并替换到项目中。确定继续?', '重新生成 Config 文件', {
|
&& (!configExpiresAt.value || configExpiresAt.value.getTime() <= Date.now())) {
|
||||||
confirmButtonText: '确定',
|
ElMessage.warning('到期时间必须晚于当前时间')
|
||||||
cancelButtonText: '取消',
|
|
||||||
type: 'warning',
|
|
||||||
})
|
|
||||||
} catch {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
regenerating.value = true
|
regenerating.value = true
|
||||||
try {
|
try {
|
||||||
await appApi.regenerateConfigFile(current.appKey)
|
const res = await appApi.regenerateConfigFile(current.appKey, {
|
||||||
|
longTerm: configExpiryMode.value === 'longTerm',
|
||||||
|
expiresAt: configExpiryMode.value === 'expiresAt' ? configExpiresAt.value!.toISOString() : null,
|
||||||
|
})
|
||||||
|
const metadata = res.data.data
|
||||||
|
app.value = {
|
||||||
|
...current,
|
||||||
|
configFileId: metadata.configId,
|
||||||
|
configFileRevision: metadata.revision,
|
||||||
|
configFileIssuedAt: metadata.issuedAt,
|
||||||
|
configFileExpiresAt: metadata.expiresAt,
|
||||||
|
}
|
||||||
|
showRegenerateDialog.value = false
|
||||||
ElMessage.success('Config 文件已重新生成,请重新下载')
|
ElMessage.success('Config 文件已重新生成,请重新下载')
|
||||||
} catch (e: any) {
|
} catch (e: any) {
|
||||||
ElMessage.error(e?.response?.data?.message || '重新生成失败')
|
ElMessage.error(e?.response?.data?.message || '重新生成失败')
|
||||||
@ -719,6 +742,7 @@ onBeforeUnmount(() => {
|
|||||||
<style scoped>
|
<style scoped>
|
||||||
.mono { font-family: monospace; font-size: 12px; }
|
.mono { font-family: monospace; font-size: 12px; }
|
||||||
.form-tip { font-size: 12px; color: var(--el-text-color-secondary); margin-top: 4px; }
|
.form-tip { font-size: 12px; color: var(--el-text-color-secondary); margin-top: 4px; }
|
||||||
|
.config-metadata { display:flex; flex-direction:column; margin-top:8px; color:var(--el-text-color-secondary); font-size:12px; line-height:1.7; }
|
||||||
.api-key-result-box {
|
.api-key-result-box {
|
||||||
font-family: monospace;
|
font-family: monospace;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<h2 style="margin-bottom: 24px">Sourcemap 管理</h2>
|
<h2 style="margin-bottom: 24px">符号化产物管理</h2>
|
||||||
|
|
||||||
<!-- App selector bar -->
|
<!-- App selector bar -->
|
||||||
<div class="app-selector-bar">
|
<div class="app-selector-bar">
|
||||||
@ -27,7 +27,7 @@
|
|||||||
<el-card shadow="never">
|
<el-card shadow="never">
|
||||||
<template #header>
|
<template #header>
|
||||||
<div class="toolbar toolbar-space-between">
|
<div class="toolbar toolbar-space-between">
|
||||||
<span>已上传的 Sourcemap 文件</span>
|
<span>已上传的符号化产物</span>
|
||||||
<el-button @click="loadData" :loading="loading">刷新</el-button>
|
<el-button @click="loadData" :loading="loading">刷新</el-button>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -38,8 +38,14 @@
|
|||||||
<el-tag size="small" :type="platformTagType(row.platform)">{{ row.platform }}</el-tag>
|
<el-tag size="small" :type="platformTagType(row.platform)">{{ row.platform }}</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<el-table-column prop="artifactType" label="产物类型" width="150" />
|
||||||
<el-table-column prop="appVersion" label="应用版本" width="120" />
|
<el-table-column prop="appVersion" label="应用版本" width="120" />
|
||||||
<el-table-column prop="bundleName" label="Bundle" width="120" />
|
<el-table-column prop="moduleId" label="模块" width="120">
|
||||||
|
<template #default="{ row }">{{ row.moduleId || '—' }}</template>
|
||||||
|
</el-table-column>
|
||||||
|
<el-table-column prop="moduleVersion" label="模块版本" width="120">
|
||||||
|
<template #default="{ row }">{{ row.moduleVersion || '—' }}</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column prop="buildId" label="Build ID" width="160" show-overflow-tooltip>
|
<el-table-column prop="buildId" label="Build ID" width="160" show-overflow-tooltip>
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<span class="buildid-text">{{ row.buildId ?? '—' }}</span>
|
<span class="buildid-text">{{ row.buildId ?? '—' }}</span>
|
||||||
@ -51,7 +57,7 @@
|
|||||||
<el-table-column label="操作" width="80" fixed="right">
|
<el-table-column label="操作" width="80" fixed="right">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-popconfirm
|
<el-popconfirm
|
||||||
title="确认删除该 Sourcemap?"
|
title="确认删除该符号化产物?"
|
||||||
confirm-button-text="删除"
|
confirm-button-text="删除"
|
||||||
cancel-button-text="取消"
|
cancel-button-text="取消"
|
||||||
confirm-button-type="danger"
|
confirm-button-type="danger"
|
||||||
@ -65,7 +71,7 @@
|
|||||||
</el-table-column>
|
</el-table-column>
|
||||||
</el-table>
|
</el-table>
|
||||||
|
|
||||||
<el-empty v-if="!loading && sourcemaps.length === 0" description="暂无 Sourcemap 文件" />
|
<el-empty v-if="!loading && sourcemaps.length === 0" description="暂无符号化产物" />
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
<el-card shadow="never" style="margin-top: 16px">
|
<el-card shadow="never" style="margin-top: 16px">
|
||||||
@ -92,10 +98,14 @@ npx react-native bundle --platform android --dev false</pre>
|
|||||||
|
|
||||||
<h4>手动上传</h4>
|
<h4>手动上传</h4>
|
||||||
<p>使用 curl 命令手动上传:</p>
|
<p>使用 curl 命令手动上传:</p>
|
||||||
<pre class="code-block">curl -X POST "https://your-api.com/bugcollect/v1/sourcemaps/upload" \
|
<pre class="code-block">curl -X POST "https://your-api.com/bugcollect/v1/artifacts/upload" \
|
||||||
|
-H "Authorization: Bearer $XUQM_API_TOKEN" \
|
||||||
|
-F "artifactType=R8_MAPPING" \
|
||||||
-F "appKey=your-app-key" \
|
-F "appKey=your-app-key" \
|
||||||
-F "platform=android" \
|
-F "platform=android" \
|
||||||
-F "appVersion=1.0.0" \
|
-F "appVersion=1.0.0" \
|
||||||
|
-F "buildId=your-build-id" \
|
||||||
|
-F "artifactHash=sha256-of-mapping" \
|
||||||
-F "file=@mapping.txt"</pre>
|
-F "file=@mapping.txt"</pre>
|
||||||
</div>
|
</div>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|||||||
@ -20,7 +20,6 @@
|
|||||||
<el-menu-item index="/services/im"><el-icon><ChatDotRound /></el-icon><span>即时通讯</span></el-menu-item>
|
<el-menu-item index="/services/im"><el-icon><ChatDotRound /></el-icon><span>即时通讯</span></el-menu-item>
|
||||||
<el-menu-item index="/services/push"><el-icon><Bell /></el-icon><span>离线推送</span></el-menu-item>
|
<el-menu-item index="/services/push"><el-icon><Bell /></el-icon><span>离线推送</span></el-menu-item>
|
||||||
<el-menu-item index="/services/update"><el-icon><Upload /></el-icon><span>版本管理</span></el-menu-item>
|
<el-menu-item index="/services/update"><el-icon><Upload /></el-icon><span>版本管理</span></el-menu-item>
|
||||||
<el-menu-item index="/services/license"><el-icon><Key /></el-icon><span>授权管理</span></el-menu-item>
|
|
||||||
<el-sub-menu index="services-bugcollect">
|
<el-sub-menu index="services-bugcollect">
|
||||||
<template #title><el-icon><DataLine /></el-icon><span>崩溃收集</span></template>
|
<template #title><el-icon><DataLine /></el-icon><span>崩溃收集</span></template>
|
||||||
<el-menu-item index="/bugcollect/overview"><el-icon><Odometer /></el-icon><span>概览</span></el-menu-item>
|
<el-menu-item index="/bugcollect/overview"><el-icon><Odometer /></el-icon><span>概览</span></el-menu-item>
|
||||||
@ -71,7 +70,6 @@
|
|||||||
<el-menu-item index="/services/im"><el-icon><ChatDotRound /></el-icon><span>即时通讯</span></el-menu-item>
|
<el-menu-item index="/services/im"><el-icon><ChatDotRound /></el-icon><span>即时通讯</span></el-menu-item>
|
||||||
<el-menu-item index="/services/push"><el-icon><Bell /></el-icon><span>离线推送</span></el-menu-item>
|
<el-menu-item index="/services/push"><el-icon><Bell /></el-icon><span>离线推送</span></el-menu-item>
|
||||||
<el-menu-item index="/services/update"><el-icon><Upload /></el-icon><span>版本管理</span></el-menu-item>
|
<el-menu-item index="/services/update"><el-icon><Upload /></el-icon><span>版本管理</span></el-menu-item>
|
||||||
<el-menu-item index="/services/license"><el-icon><Key /></el-icon><span>授权管理</span></el-menu-item>
|
|
||||||
<el-sub-menu index="services-bugcollect">
|
<el-sub-menu index="services-bugcollect">
|
||||||
<template #title><el-icon><DataLine /></el-icon><span>崩溃收集</span></template>
|
<template #title><el-icon><DataLine /></el-icon><span>崩溃收集</span></template>
|
||||||
<el-menu-item index="/bugcollect/overview"><el-icon><Odometer /></el-icon><span>概览</span></el-menu-item>
|
<el-menu-item index="/bugcollect/overview"><el-icon><Odometer /></el-icon><span>概览</span></el-menu-item>
|
||||||
|
|||||||
@ -1,380 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div v-if="isServicesPortal" class="portal-bar">
|
|
||||||
<span class="portal-bar-title">授权管理</span>
|
|
||||||
<el-select :model-value="appKey" placeholder="选择应用" style="width:220px" @change="switchApp">
|
|
||||||
<el-option v-for="a in portalApps" :key="a.appKey" :label="a.name" :value="a.appKey" />
|
|
||||||
</el-select>
|
|
||||||
</div>
|
|
||||||
<el-page-header v-else @back="$router.back()" :content="`授权管理 - ${appName}`" style="margin-bottom:20px" />
|
|
||||||
<el-empty v-if="isServicesPortal && !appKey" description="请选择一个应用" style="margin-top:80px" />
|
|
||||||
|
|
||||||
<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-col :xs="24" :sm="12" :md="6" v-for="item in statCards" :key="item.label">
|
|
||||||
<el-card shadow="never">
|
|
||||||
<div class="stat-card">
|
|
||||||
<span class="stat-value">{{ item.value }}</span>
|
|
||||||
<span class="stat-label">{{ item.label }}</span>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
|
|
||||||
<el-card shadow="never" style="margin-top:16px">
|
|
||||||
<div class="license-summary">
|
|
||||||
<el-descriptions :column="isMobile ? 1 : 3" border>
|
|
||||||
<el-descriptions-item label="应用">{{ appName }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="AppKey">{{ appKey || '-' }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="授权状态">
|
|
||||||
<el-tag :type="license?.isActive ? 'success' : 'danger'" size="small">
|
|
||||||
{{ license?.isActive ? '正常' : '停用' }}
|
|
||||||
</el-tag>
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="最大设备数">
|
|
||||||
<div v-if="!editingMaxDevices" class="max-devices-display">
|
|
||||||
<span>{{ license?.maxDevices ?? '-' }}</span>
|
|
||||||
<el-button link type="primary" size="small" style="margin-left:8px" @click="startEditMaxDevices">修改</el-button>
|
|
||||||
</div>
|
|
||||||
<div v-else class="max-devices-edit">
|
|
||||||
<el-input-number v-model="editMaxDevicesValue" :min="1" :max="999999" size="small" />
|
|
||||||
<el-button type="primary" size="small" :loading="savingMaxDevices" style="margin-left:8px" @click="saveMaxDevices">保存</el-button>
|
|
||||||
<el-button size="small" style="margin-left:4px" @click="editingMaxDevices = false">取消</el-button>
|
|
||||||
</div>
|
|
||||||
</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="已注册设备">{{ license?.registeredDevices ?? devices.length }}</el-descriptions-item>
|
|
||||||
<el-descriptions-item label="过期时间">
|
|
||||||
<div v-if="!editingExpiresAt" class="max-devices-display">
|
|
||||||
<span>{{ license?.expiresAt ? formatDate(license.expiresAt) : '永久' }}</span>
|
|
||||||
<el-button link type="primary" size="small" style="margin-left:8px" @click="startEditExpiresAt">修改</el-button>
|
|
||||||
</div>
|
|
||||||
<div v-else class="max-devices-edit">
|
|
||||||
<el-date-picker
|
|
||||||
v-model="editExpiresAtValue"
|
|
||||||
type="datetime"
|
|
||||||
placeholder="留空表示永久"
|
|
||||||
clearable
|
|
||||||
style="width:200px"
|
|
||||||
size="small"
|
|
||||||
/>
|
|
||||||
<el-button type="primary" size="small" :loading="savingExpiresAt" style="margin-left:8px" @click="saveExpiresAt">保存</el-button>
|
|
||||||
<el-button size="small" style="margin-left:4px" @click="editingExpiresAt = false">取消</el-button>
|
|
||||||
</div>
|
|
||||||
</el-descriptions-item>
|
|
||||||
</el-descriptions>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="toolbar responsive-toolbar">
|
|
||||||
<el-button @click="loadData" :loading="loading">刷新</el-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="table-wrap">
|
|
||||||
<el-table :data="devices" v-loading="loading" border stripe>
|
|
||||||
<el-table-column prop="deviceId" label="设备ID" min-width="220" />
|
|
||||||
<el-table-column prop="deviceName" label="设备名称" min-width="130">
|
|
||||||
<template #default="{ row }">{{ row.deviceName || '-' }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="用户" min-width="180">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<div class="stack-cell">
|
|
||||||
<span>{{ row.userName || row.userId || '-' }}</span>
|
|
||||||
<small v-if="row.userName && row.userId">{{ row.userId }}</small>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="联系方式" min-width="190">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<div class="stack-cell">
|
|
||||||
<span>{{ row.userPhone || '-' }}</span>
|
|
||||||
<small v-if="row.userEmail">{{ row.userEmail }}</small>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="设备信息" min-width="190">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<div class="stack-cell">
|
|
||||||
<span>{{ [row.deviceVendor, row.deviceModel].filter(Boolean).join(' ') || '-' }}</span>
|
|
||||||
<small v-if="row.osVersion">{{ row.osVersion }}</small>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="注册时间" width="170">
|
|
||||||
<template #default="{ row }">{{ formatDate(row.registeredAt) }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="最后验证" width="170">
|
|
||||||
<template #default="{ row }">{{ row.lastVerifiedAt ? formatDate(row.lastVerifiedAt) : '-' }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="状态" width="90">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-tag :type="row.isActive ? 'success' : 'danger'" size="small">
|
|
||||||
{{ row.isActive ? '正常' : '吊销' }}
|
|
||||||
</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" width="120" fixed="right">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-button v-if="row.isActive" size="small" type="danger" plain @click="revokeDevice(row)">吊销</el-button>
|
|
||||||
<el-button v-else size="small" type="success" plain @click="reactivateDevice(row)">激活</el-button>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
|
||||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
|
||||||
import { licenseApi, type AppLicense, type LicenseDevice } from '@/api/license'
|
|
||||||
import { appApi, type App } from '@/api/app'
|
|
||||||
import { formatTime } from '@/utils/date'
|
|
||||||
import { getLastServiceApp, saveLastServiceApp } from '@/utils/serviceApp'
|
|
||||||
import {
|
|
||||||
connectServiceActivationRealtime,
|
|
||||||
disconnectServiceActivationRealtime,
|
|
||||||
type ServiceActivationRefreshEvent,
|
|
||||||
} from '@/services/storeReviewRealtime'
|
|
||||||
|
|
||||||
const route = useRoute()
|
|
||||||
const router = useRouter()
|
|
||||||
|
|
||||||
const isMobile = ref(false)
|
|
||||||
const appKey = computed(() => (route.params.appKey as string) || (route.query.appKey as string) || '')
|
|
||||||
const isServicesPortal = computed(() => route.path.startsWith('/services/'))
|
|
||||||
|
|
||||||
const serviceEnabled = ref<boolean | null>(null)
|
|
||||||
const checkingService = ref(false)
|
|
||||||
const showActivationDialog = ref(false)
|
|
||||||
const activationReason = ref('')
|
|
||||||
const submittingActivation = ref(false)
|
|
||||||
|
|
||||||
const portalApps = ref<App[]>([])
|
|
||||||
const currentApp = ref<App | null>(null)
|
|
||||||
const license = ref<AppLicense | null>(null)
|
|
||||||
const devices = ref<LicenseDevice[]>([])
|
|
||||||
const loading = ref(false)
|
|
||||||
const editingMaxDevices = ref(false)
|
|
||||||
const editMaxDevicesValue = ref(1)
|
|
||||||
const savingMaxDevices = ref(false)
|
|
||||||
const editingExpiresAt = ref(false)
|
|
||||||
const editExpiresAtValue = ref<Date | null>(null)
|
|
||||||
const savingExpiresAt = ref(false)
|
|
||||||
|
|
||||||
const appName = computed(() => currentApp.value?.name || license.value?.name || appKey.value || '-')
|
|
||||||
const activeDevices = computed(() => devices.value.filter(d => d.isActive).length)
|
|
||||||
const revokedDevices = computed(() => devices.value.filter(d => !d.isActive).length)
|
|
||||||
|
|
||||||
const statCards = computed(() => [
|
|
||||||
{ label: '最大设备数', value: license.value?.maxDevices ?? '-' },
|
|
||||||
{ label: '已注册设备', value: license.value?.registeredDevices ?? devices.value.length },
|
|
||||||
{ label: '正常设备', value: activeDevices.value },
|
|
||||||
{ label: '吊销设备', value: revokedDevices.value },
|
|
||||||
])
|
|
||||||
|
|
||||||
async function loadData() {
|
|
||||||
const key = appKey.value
|
|
||||||
if (!key) return
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const [licenseRes, appRes] = await Promise.all([
|
|
||||||
licenseApi.getAppLicense(key),
|
|
||||||
appApi.get(key).catch(() => null),
|
|
||||||
])
|
|
||||||
license.value = licenseRes.data.data.license
|
|
||||||
devices.value = licenseRes.data.data.devices || []
|
|
||||||
currentApp.value = appRes?.data.data ?? portalApps.value.find(item => item.appKey === key) ?? null
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function startEditMaxDevices() {
|
|
||||||
editMaxDevicesValue.value = license.value?.maxDevices ?? 1
|
|
||||||
editingMaxDevices.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveMaxDevices() {
|
|
||||||
const key = appKey.value
|
|
||||||
if (!key) return
|
|
||||||
savingMaxDevices.value = true
|
|
||||||
try {
|
|
||||||
const res = await licenseApi.updateAppLicense(key, { maxDevices: editMaxDevicesValue.value })
|
|
||||||
license.value = res.data.data
|
|
||||||
editingMaxDevices.value = false
|
|
||||||
ElMessage.success('最大设备数已更新')
|
|
||||||
} catch {
|
|
||||||
ElMessage.error('更新失败')
|
|
||||||
} finally {
|
|
||||||
savingMaxDevices.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function startEditExpiresAt() {
|
|
||||||
editExpiresAtValue.value = license.value?.expiresAt ? new Date(license.value.expiresAt) : null
|
|
||||||
editingExpiresAt.value = true
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveExpiresAt() {
|
|
||||||
const key = appKey.value
|
|
||||||
if (!key) return
|
|
||||||
savingExpiresAt.value = true
|
|
||||||
try {
|
|
||||||
const expiresAt = editExpiresAtValue.value ? editExpiresAtValue.value.toISOString() : ''
|
|
||||||
const res = await licenseApi.updateAppLicense(key, { expiresAt })
|
|
||||||
license.value = res.data.data
|
|
||||||
editingExpiresAt.value = false
|
|
||||||
ElMessage.success('过期时间已更新')
|
|
||||||
} catch {
|
|
||||||
ElMessage.error('更新失败')
|
|
||||||
} finally {
|
|
||||||
savingExpiresAt.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function revokeDevice(row: LicenseDevice) {
|
|
||||||
try {
|
|
||||||
await ElMessageBox.confirm('确认吊销该设备?', '吊销确认', { type: 'warning' })
|
|
||||||
await licenseApi.revokeDevice(row.id)
|
|
||||||
ElMessage.success('已吊销')
|
|
||||||
loadData()
|
|
||||||
} catch (e) {
|
|
||||||
// cancelled
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function reactivateDevice(row: LicenseDevice) {
|
|
||||||
await licenseApi.reactivateDevice(row.id)
|
|
||||||
ElMessage.success('已激活')
|
|
||||||
loadData()
|
|
||||||
}
|
|
||||||
|
|
||||||
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 === 'LICENSE' && s.enabled)
|
|
||||||
serviceEnabled.value = enabled
|
|
||||||
if (enabled) loadData()
|
|
||||||
} 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, 'LICENSE', activationReason.value.trim())
|
|
||||||
ElMessage.success('申请已提交,等待运营审核')
|
|
||||||
showActivationDialog.value = false
|
|
||||||
activationReason.value = ''
|
|
||||||
} catch {
|
|
||||||
ElMessage.error('提交失败')
|
|
||||||
} finally {
|
|
||||||
submittingActivation.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function switchApp(key: string) {
|
|
||||||
saveLastServiceApp('license', key)
|
|
||||||
router.push(`/services/license/${key}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatDate = formatTime
|
|
||||||
|
|
||||||
function updateViewport() {
|
|
||||||
isMobile.value = window.innerWidth < 768
|
|
||||||
}
|
|
||||||
|
|
||||||
function connectLicenseRealtime(key: string) {
|
|
||||||
void connectServiceActivationRealtime(key, (event: ServiceActivationRefreshEvent) => {
|
|
||||||
if (event.serviceType !== 'LICENSE') return
|
|
||||||
if (event.status === 'APPROVED') {
|
|
||||||
ElMessage.success('授权管理服务已审核通过')
|
|
||||||
checkServiceEnabled(key)
|
|
||||||
} else if (event.status === 'REJECTED') {
|
|
||||||
ElMessage.error(`授权管理服务审核未通过:${event.reviewNote || '请联系运营'}`)
|
|
||||||
checkServiceEnabled(key)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
watch(appKey, (key) => {
|
|
||||||
if (isServicesPortal.value) {
|
|
||||||
if (key) checkServiceEnabled(key)
|
|
||||||
} else {
|
|
||||||
loadData()
|
|
||||||
}
|
|
||||||
if (key) connectLicenseRealtime(key)
|
|
||||||
})
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
updateViewport()
|
|
||||||
window.addEventListener('resize', updateViewport)
|
|
||||||
if (isServicesPortal.value) {
|
|
||||||
appApi.list().then(res => {
|
|
||||||
portalApps.value = res.data.data || []
|
|
||||||
currentApp.value = portalApps.value.find(item => item.appKey === appKey.value) ?? currentApp.value
|
|
||||||
if (!appKey.value && portalApps.value.length) {
|
|
||||||
const saved = getLastServiceApp('license')
|
|
||||||
const target = saved && portalApps.value.some(a => a.appKey === saved) ? saved : portalApps.value[0].appKey
|
|
||||||
switchApp(target)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
if (appKey.value) checkServiceEnabled(appKey.value)
|
|
||||||
} else {
|
|
||||||
loadData()
|
|
||||||
}
|
|
||||||
if (appKey.value) connectLicenseRealtime(appKey.value)
|
|
||||||
})
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
window.removeEventListener('resize', updateViewport)
|
|
||||||
disconnectServiceActivationRealtime()
|
|
||||||
})
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.stat-grid { margin-bottom: 16px; }
|
|
||||||
.stat-card { display: flex; flex-direction: column; align-items: center; padding: 12px; }
|
|
||||||
.stat-value { font-size: 28px; font-weight: 700; color: #0E7BF2; }
|
|
||||||
.stat-label { font-size: 13px; color: #64748b; margin-top: 4px; }
|
|
||||||
.license-summary { margin-bottom: 16px; }
|
|
||||||
.toolbar { margin-bottom: 12px; display: flex; gap: 8px; flex-wrap: wrap; }
|
|
||||||
.table-wrap { overflow-x: auto; }
|
|
||||||
.portal-bar { display: flex; align-items: center; gap: 12px; margin-bottom: 20px; }
|
|
||||||
.portal-bar-title { font-size: 18px; font-weight: 600; }
|
|
||||||
.stack-cell { display: flex; flex-direction: column; gap: 2px; line-height: 1.35; }
|
|
||||||
.stack-cell small { color: #909399; font-size: 12px; }
|
|
||||||
.max-devices-display { display: flex; align-items: center; }
|
|
||||||
.max-devices-edit { display: flex; align-items: center; }
|
|
||||||
</style>
|
|
||||||
@ -239,36 +239,6 @@
|
|||||||
@current-change="handlePushPageChange" />
|
@current-change="handlePushPageChange" />
|
||||||
</el-tab-pane>
|
</el-tab-pane>
|
||||||
|
|
||||||
<!-- ── 设备授权 ───────────────────────────────────────────── -->
|
|
||||||
<el-tab-pane label="设备授权" name="LICENSE">
|
|
||||||
<div class="toolbar responsive-toolbar">
|
|
||||||
<el-select v-model="licenseAppKey" placeholder="选择应用" style="width: 320px" filterable @change="handleLicenseAppChange">
|
|
||||||
<el-option v-for="app in apps" :key="app.appKey" :label="`${app.name} · ${app.packageName}`" :value="app.appKey" />
|
|
||||||
</el-select>
|
|
||||||
<el-button :loading="licenseLoading" @click="loadLicenseLogs">刷新</el-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="table-wrap">
|
|
||||||
<el-table :data="licenseLogs" v-loading="licenseLoading" border stripe>
|
|
||||||
<el-table-column prop="createdAt" label="时间" width="170">
|
|
||||||
<template #default="{ row }"><span class="time-text">{{ formatTime(row.createdAt) }}</span></template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作" width="160">
|
|
||||||
<template #default="{ row }">
|
|
||||||
<el-tag size="small" :type="actionTagType(row.action)" effect="dark">{{ actionLabel(row.action) }}</el-tag>
|
|
||||||
</template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column label="操作详情" min-width="360">
|
|
||||||
<template #default="{ row }"><span class="summary-text">{{ licenseSummaryText(row) }}</span></template>
|
|
||||||
</el-table-column>
|
|
||||||
<el-table-column prop="operator" label="操作人" width="120" />
|
|
||||||
</el-table>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<el-pagination style="margin-top: 16px" layout="total, prev, pager, next"
|
|
||||||
:total="licenseTotal" :page-size="licensePageSize" :current-page="licensePage + 1"
|
|
||||||
@current-change="handleLicensePageChange" />
|
|
||||||
</el-tab-pane>
|
|
||||||
</el-tabs>
|
</el-tabs>
|
||||||
</el-card>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
@ -281,10 +251,9 @@ import { updateAdminApi, type OperationLog as UpdateOperationLog } from '@/api/u
|
|||||||
import { operationLogApi, type TenantOperationLog } from '@/api/operationLog'
|
import { operationLogApi, type TenantOperationLog } from '@/api/operationLog'
|
||||||
import { imAdminApi, type OperationLog as ImOperationLog } from '@/api/im'
|
import { imAdminApi, type OperationLog as ImOperationLog } from '@/api/im'
|
||||||
import { pushAdminApi, type PushOperationLog } from '@/api/push'
|
import { pushAdminApi, type PushOperationLog } from '@/api/push'
|
||||||
import { licenseApi, type LicenseOperationLog } from '@/api/license'
|
|
||||||
import { formatTime } from '@/utils/date'
|
import { formatTime } from '@/utils/date'
|
||||||
|
|
||||||
const activeSource = ref<'TENANT' | 'IM' | 'UPDATE' | 'PUSH' | 'LICENSE'>('TENANT')
|
const activeSource = ref<'TENANT' | 'IM' | 'UPDATE' | 'PUSH'>('TENANT')
|
||||||
|
|
||||||
// ── 租户平台 state ───────────────────────────────────────────────────────────
|
// ── 租户平台 state ───────────────────────────────────────────────────────────
|
||||||
|
|
||||||
@ -321,15 +290,6 @@ const pushPage = ref(0)
|
|||||||
const pushPageSize = ref(20)
|
const pushPageSize = ref(20)
|
||||||
const pushAppKey = ref('')
|
const pushAppKey = ref('')
|
||||||
|
|
||||||
// ── 授权 state ─────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
const licenseLoading = ref(false)
|
|
||||||
const licenseLogs = ref<LicenseOperationLog[]>([])
|
|
||||||
const licenseTotal = ref(0)
|
|
||||||
const licensePage = ref(0)
|
|
||||||
const licensePageSize = ref(20)
|
|
||||||
const licenseAppKey = ref('')
|
|
||||||
|
|
||||||
// ── 初始化 ──────────────────────────────────────────────────────────────────
|
// ── 初始化 ──────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
@ -337,13 +297,12 @@ onMounted(async () => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
watch(activeSource, async (value) => {
|
watch(activeSource, async (value) => {
|
||||||
if (['UPDATE', 'IM', 'PUSH', 'LICENSE'].includes(value) && !apps.value.length) {
|
if (['UPDATE', 'IM', 'PUSH'].includes(value) && !apps.value.length) {
|
||||||
await loadApps()
|
await loadApps()
|
||||||
}
|
}
|
||||||
if (value === 'UPDATE') await loadUpdateLogs()
|
if (value === 'UPDATE') await loadUpdateLogs()
|
||||||
if (value === 'IM' && imAppKey.value) await loadImLogs()
|
if (value === 'IM' && imAppKey.value) await loadImLogs()
|
||||||
if (value === 'PUSH' && pushAppKey.value) await loadPushLogs()
|
if (value === 'PUSH' && pushAppKey.value) await loadPushLogs()
|
||||||
if (value === 'LICENSE' && licenseAppKey.value) await loadLicenseLogs()
|
|
||||||
})
|
})
|
||||||
|
|
||||||
// ── 数据加载 ────────────────────────────────────────────────────────────────
|
// ── 数据加载 ────────────────────────────────────────────────────────────────
|
||||||
@ -356,7 +315,6 @@ async function loadApps() {
|
|||||||
if (!updateAppKey.value && apps.value.length) updateAppKey.value = apps.value[0].appKey
|
if (!updateAppKey.value && apps.value.length) updateAppKey.value = apps.value[0].appKey
|
||||||
if (!imAppKey.value && apps.value.length) imAppKey.value = apps.value[0].appKey
|
if (!imAppKey.value && apps.value.length) imAppKey.value = apps.value[0].appKey
|
||||||
if (!pushAppKey.value && apps.value.length) pushAppKey.value = apps.value[0].appKey
|
if (!pushAppKey.value && apps.value.length) pushAppKey.value = apps.value[0].appKey
|
||||||
if (!licenseAppKey.value && apps.value.length) licenseAppKey.value = apps.value[0].appKey
|
|
||||||
} catch { /* ignore */ }
|
} catch { /* ignore */ }
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -413,19 +371,6 @@ async function loadPushLogs() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function loadLicenseLogs() {
|
|
||||||
if (!licenseAppKey.value) { licenseLogs.value = []; return }
|
|
||||||
licenseLoading.value = true
|
|
||||||
try {
|
|
||||||
const res = await licenseApi.getOperationLogs(licenseAppKey.value, licensePage.value, licensePageSize.value)
|
|
||||||
const data = res.data.data
|
|
||||||
licenseLogs.value = data.content ?? []
|
|
||||||
licenseTotal.value = data.total ?? 0
|
|
||||||
} finally {
|
|
||||||
licenseLoading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 事件处理 ────────────────────────────────────────────────────────────────
|
// ── 事件处理 ────────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function handleTenantPageChange(nextPage: number) {
|
function handleTenantPageChange(nextPage: number) {
|
||||||
@ -462,16 +407,6 @@ function handlePushPageChange(nextPage: number) {
|
|||||||
loadPushLogs()
|
loadPushLogs()
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleLicenseAppChange() {
|
|
||||||
licensePage.value = 0
|
|
||||||
loadLicenseLogs()
|
|
||||||
}
|
|
||||||
|
|
||||||
function handleLicensePageChange(nextPage: number) {
|
|
||||||
licensePage.value = nextPage - 1
|
|
||||||
loadLicenseLogs()
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 通用标签映射 ────────────────────────────────────────────────────────────
|
// ── 通用标签映射 ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function moduleLabel(moduleType: string): string {
|
function moduleLabel(moduleType: string): string {
|
||||||
@ -604,10 +539,6 @@ function actionLabel(action: string): string {
|
|||||||
DELETE_USER: '删除用户',
|
DELETE_USER: '删除用户',
|
||||||
IMPORT_USER: '导入用户',
|
IMPORT_USER: '导入用户',
|
||||||
TEST_PUSH: '发送测试推送',
|
TEST_PUSH: '发送测试推送',
|
||||||
// ── license-service: 授权管理 ──
|
|
||||||
UPDATE_LICENSE: '更新授权配置',
|
|
||||||
REVOKE_DEVICE: '吊销设备授权',
|
|
||||||
REACTIVATE_DEVICE: '重新激活设备',
|
|
||||||
}
|
}
|
||||||
return map[action] ?? action
|
return map[action] ?? action
|
||||||
}
|
}
|
||||||
@ -655,9 +586,6 @@ function resourceTypeLabel(resourceType: string): string {
|
|||||||
SERVICE: '服务配置',
|
SERVICE: '服务配置',
|
||||||
// push-service
|
// push-service
|
||||||
PUSH: '推送消息',
|
PUSH: '推送消息',
|
||||||
// license-service
|
|
||||||
LICENSE: '授权许可',
|
|
||||||
DEVICE: '设备',
|
|
||||||
}[resourceType] ?? resourceType
|
}[resourceType] ?? resourceType
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -688,16 +616,6 @@ function pushSummaryText(row: PushOperationLog): string {
|
|||||||
return `${act}(${res})`
|
return `${act}(${res})`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── 授权摘要 ────────────────────────────────────────────────────────────────
|
|
||||||
|
|
||||||
function licenseSummaryText(row: LicenseOperationLog): string {
|
|
||||||
if (row.summary) return row.summary
|
|
||||||
const act = actionLabel(row.action)
|
|
||||||
const res = resourceTypeLabel(row.resourceType)
|
|
||||||
if (row.resourceId) return `${act} — ${res} ${row.resourceId}`
|
|
||||||
return `${act}(${res})`
|
|
||||||
}
|
|
||||||
|
|
||||||
// ── 版本管理摘要 ────────────────────────────────────────────────────────────
|
// ── 版本管理摘要 ────────────────────────────────────────────────────────────
|
||||||
|
|
||||||
function updateSummaryText(row: UpdateOperationLog): string {
|
function updateSummaryText(row: UpdateOperationLog): string {
|
||||||
|
|||||||
@ -140,7 +140,10 @@
|
|||||||
<el-descriptions-item label="Android 包名">{{ configParseResult.packageName || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="Android 包名">{{ configParseResult.packageName || '-' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="iOS BundleId">{{ configParseResult.iosBundleId || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="iOS BundleId">{{ configParseResult.iosBundleId || '-' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="鸿蒙 BundleName">{{ configParseResult.harmonyBundleName || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="鸿蒙 BundleName">{{ configParseResult.harmonyBundleName || '-' }}</el-descriptions-item>
|
||||||
<el-descriptions-item label="服务地址">{{ configParseResult.serverUrl || configParseResult.baseUrl || '-' }}</el-descriptions-item>
|
<el-descriptions-item label="Config 版本">{{ configParseResult.revision }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="签发时间">{{ formatTime(configParseResult.issuedAt) }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="到期时间">{{ configParseResult.expiresAt ? formatTime(configParseResult.expiresAt) : '长期有效' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="服务地址">{{ configParseResult.serverUrl || '-' }}</el-descriptions-item>
|
||||||
</el-descriptions>
|
</el-descriptions>
|
||||||
</el-card>
|
</el-card>
|
||||||
|
|
||||||
@ -285,12 +288,16 @@ const deploymentMode = ref<'PUBLIC' | 'PRIVATE' | null>(null)
|
|||||||
const configFileContent = ref('')
|
const configFileContent = ref('')
|
||||||
const parsingConfig = ref(false)
|
const parsingConfig = ref(false)
|
||||||
const configParseResult = ref<{
|
const configParseResult = ref<{
|
||||||
|
schemaVersion: number
|
||||||
|
configId: string
|
||||||
|
revision: number
|
||||||
|
issuedAt: string
|
||||||
|
expiresAt: string | null
|
||||||
appKey: string
|
appKey: string
|
||||||
appName: string
|
appName: string
|
||||||
packageName: string
|
packageName: string
|
||||||
iosBundleId: string
|
iosBundleId: string
|
||||||
harmonyBundleName: string
|
harmonyBundleName: string
|
||||||
baseUrl: string
|
|
||||||
serverUrl: string
|
serverUrl: string
|
||||||
} | null>(null)
|
} | null>(null)
|
||||||
const configUploadRef = ref<any>(null)
|
const configUploadRef = ref<any>(null)
|
||||||
@ -418,7 +425,6 @@ const SERVICE_DISPLAY_NAMES: Record<string, string> = {
|
|||||||
'push-service': '推送服务',
|
'push-service': '推送服务',
|
||||||
'update-service': '版本管理服务',
|
'update-service': '版本管理服务',
|
||||||
'file-service': '文件服务',
|
'file-service': '文件服务',
|
||||||
'license-service': '授权服务',
|
|
||||||
'xuqm-bugcollect-service': '崩溃收集服务',
|
'xuqm-bugcollect-service': '崩溃收集服务',
|
||||||
'demo-service': 'Demo 服务',
|
'demo-service': 'Demo 服务',
|
||||||
'tenant-web': '租户平台 (Web)',
|
'tenant-web': '租户平台 (Web)',
|
||||||
|
|||||||
@ -46,10 +46,6 @@ export default defineConfig(({ mode }) => {
|
|||||||
target: 'http://127.0.0.1:8087',
|
target: 'http://127.0.0.1:8087',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
},
|
},
|
||||||
'/api/license': {
|
|
||||||
target: 'http://127.0.0.1:8085',
|
|
||||||
changeOrigin: true,
|
|
||||||
},
|
|
||||||
'/api/push': {
|
'/api/push': {
|
||||||
target: 'http://127.0.0.1:8083',
|
target: 'http://127.0.0.1:8083',
|
||||||
changeOrigin: true,
|
changeOrigin: true,
|
||||||
|
|||||||
@ -1432,7 +1432,7 @@
|
|||||||
"@xuqm/vue3-sdk@0.2.3", "@xuqm/vue3-sdk@^0.2.3":
|
"@xuqm/vue3-sdk@0.2.3", "@xuqm/vue3-sdk@^0.2.3":
|
||||||
version "0.2.3"
|
version "0.2.3"
|
||||||
resolved "https://nexus.xuqinmin.com/repository/npm-hosted/@xuqm/vue3-sdk/-/vue3-sdk-0.2.3.tgz#b0100a309bf4179a43d98e21e24759b020221797"
|
resolved "https://nexus.xuqinmin.com/repository/npm-hosted/@xuqm/vue3-sdk/-/vue3-sdk-0.2.3.tgz#b0100a309bf4179a43d98e21e24759b020221797"
|
||||||
integrity sha512-lUQSQNaYyrji8WwL/N4Xswdzyo4MrbwqSHsghC2Sg/zEGtuTAxo+D7t8pK1wTRLXwR5P2ZutYhE75OIFhXMFLg==
|
integrity "sha1-tkzePZdiSzSq2XYRrs7o/kRWbig= sha512-R7s9Hups0ng7IsYi3oCrV77O0CI3r3IiRnp0h94bHJoaCqFiMY8sdXvCJTGGkNymkSLcQf17viGHH1HbdoMziA=="
|
||||||
|
|
||||||
abbrev@^2.0.0:
|
abbrev@^2.0.0:
|
||||||
version "2.0.0"
|
version "2.0.0"
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户