docs: 添加 SDK API 重设计、安全设计规范和测试进度跟踪文档
- 新增 SDK API 重设计规范文档,统一各端 SDK 初始化、登录、消息接口 - 新增安全设计规范文档,涵盖密码安全、AppSecret 验证、令牌存储等安全要点 - 新增 Bug 跟踪记录文档,记录已修复问题和开放问题 - 新增测试进度跟踪文档,记录各模块测试覆盖情况和验证结果
这个提交包含在:
父节点
e5552044ae
当前提交
af253f688a
@ -44,7 +44,6 @@ yarn workspace ops-platform dev
|
|||||||
| `/apps/:id` | AppDetailView | 应用详情(需登录) |
|
| `/apps/:id` | AppDetailView | 应用详情(需登录) |
|
||||||
| `/security` | SecurityCenterView | 安全中心(需登录) |
|
| `/security` | SecurityCenterView | 安全中心(需登录) |
|
||||||
| `/docs` | DocsCenterView | 接入文档(需登录) |
|
| `/docs` | DocsCenterView | 接入文档(需登录) |
|
||||||
| `/packages` | BillingView | 服务套餐 / 配额(需登录) |
|
|
||||||
| `/accounts` | SubAccountView | 子账号管理(需登录) |
|
| `/accounts` | SubAccountView | 子账号管理(需登录) |
|
||||||
|
|
||||||
### 认证流程
|
### 认证流程
|
||||||
@ -94,11 +93,10 @@ JWT Payload 由 `atob(token.split('.')[1])` 解析,无需额外请求。
|
|||||||
- 每个平台下显示 `IM` / `推送` / `版本管理` 三个服务卡片
|
- 每个平台下显示 `IM` / `推送` / `版本管理` 三个服务卡片
|
||||||
- 支持一键开关服务、复制 secretKey、重新生成 secretKey
|
- 支持一键开关服务、复制 secretKey、重新生成 secretKey
|
||||||
|
|
||||||
### 安全中心 / 接入文档 / 服务套餐
|
### 安全中心 / 接入文档
|
||||||
|
|
||||||
- 安全中心提供 AppSecret 查看/重置入口,直接复用邮箱验证码流程
|
- 安全中心提供 AppSecret 查看/重置入口,直接复用邮箱验证码流程
|
||||||
- 接入文档页提供 RN、Android / iOS 和服务端的最短接入示例
|
- 接入文档页提供 RN、Android / iOS 和服务端的最短接入示例
|
||||||
- 服务套餐页展示当前租户的配额和服务开通概览,不涉及费用计费
|
|
||||||
|
|
||||||
### 子账号管理(SubAccountView)
|
### 子账号管理(SubAccountView)
|
||||||
|
|
||||||
|
|||||||
@ -52,7 +52,7 @@ XuqmSDK.initialize(
|
|||||||
|
|
||||||
```kotlin
|
```kotlin
|
||||||
// 登录(协程 suspend 函数)
|
// 登录(协程 suspend 函数)
|
||||||
// 只需要 userId + userSig,不需要 nickname / avatar / 过期字段
|
// 只需要 userId + userSig,不需要 nickname / avatar / 生命周期字段
|
||||||
lifecycleScope.launch {
|
lifecycleScope.launch {
|
||||||
XuqmSDK.login(
|
XuqmSDK.login(
|
||||||
userId = "user_001",
|
userId = "user_001",
|
||||||
@ -160,7 +160,7 @@ XuqmSDK.logout()
|
|||||||
// ↓ 自动触发所有模块清理
|
// ↓ 自动触发所有模块清理
|
||||||
```
|
```
|
||||||
|
|
||||||
> **注意**:`userSig` 由业务服务端签发,SDK 侧不做续签。若需更新登录态,请直接重新登录;如需撤销权限,可在租户平台重置 `appSecret` 或拉黑账号。
|
> **注意**:`userSig` 由业务服务端签发。若需更新登录态,请直接重新登录;如需撤销权限,可在租户平台重置 `appSecret` 或拉黑账号。
|
||||||
|
|
||||||
### 8. 版本更新
|
### 8. 版本更新
|
||||||
|
|
||||||
|
|||||||
@ -62,7 +62,7 @@ XuqmSDK.shared.initialize(config: config)
|
|||||||
import XuqmIM
|
import XuqmIM
|
||||||
|
|
||||||
// 使用 UserSig 登录(推荐生产环境)
|
// 使用 UserSig 登录(推荐生产环境)
|
||||||
// 只需要 userId + userSig,不需要 nickname / avatar / 过期字段
|
// 只需要 userId + userSig,不需要 nickname / avatar / 生命周期字段
|
||||||
try await XuqmSDK.shared.login(userId: "user_001", userSig: "your_user_sig_jwt")
|
try await XuqmSDK.shared.login(userId: "user_001", userSig: "your_user_sig_jwt")
|
||||||
|
|
||||||
// 设置事件代理
|
// 设置事件代理
|
||||||
@ -203,7 +203,7 @@ try await XuqmSDK.shared.login(userId: "user_001", userSig: userSig)
|
|||||||
XuqmSDK.shared.logout()
|
XuqmSDK.shared.logout()
|
||||||
```
|
```
|
||||||
|
|
||||||
> **注意**:`userSig` 由业务服务端签发,SDK 侧不做续签。若需更新登录态,请直接重新登录。
|
> **注意**:`userSig` 由业务服务端签发。若需更新登录态,请直接重新登录。
|
||||||
|
|
||||||
### 10. 检查更新
|
### 10. 检查更新
|
||||||
|
|
||||||
|
|||||||
@ -280,7 +280,7 @@ UserSig 生成方式见 [安全设计文档](../../design/02-security-design.md)
|
|||||||
XuqmGroup 是托管平台,服务地址统一管理,与腾讯云 IM 等平台的设计一致,开发者只需关心业务逻辑。
|
XuqmGroup 是托管平台,服务地址统一管理,与腾讯云 IM 等平台的设计一致,开发者只需关心业务逻辑。
|
||||||
|
|
||||||
**Q: UserSig 是什么?**
|
**Q: UserSig 是什么?**
|
||||||
UserSig 是您的业务服务端用 AppSecret 为用户签发的安全凭证,有效期可配置。AppSecret 绝不下发到客户端。
|
UserSig 是您的业务服务端用 AppSecret 为用户签发的安全凭证。当前项目的 IM 登录不做过期功能,只校验 `userId + UserSig` 是否匹配。AppSecret 绝不下发到客户端。
|
||||||
|
|
||||||
**Q: 本地消息存储在哪里?**
|
**Q: 本地消息存储在哪里?**
|
||||||
使用 WatermelonDB(SQLite),按 `appKey + userId` 自动隔离,多账号切换安全。
|
使用 WatermelonDB(SQLite),按 `appKey + userId` 自动隔离,多账号切换安全。
|
||||||
|
|||||||
@ -50,7 +50,7 @@ POST /api/im/auth/login
|
|||||||
{ "token": "eyJ..." }
|
{ "token": "eyJ..." }
|
||||||
```
|
```
|
||||||
|
|
||||||
> SDK 侧不做续签;登录态更新由业务侧重新登录完成。
|
> 登录态更新由业务侧重新登录完成。
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@ -41,10 +41,6 @@ const router = createRouter({
|
|||||||
path: 'docs',
|
path: 'docs',
|
||||||
component: () => import('@/views/docs/DocsCenterView.vue'),
|
component: () => import('@/views/docs/DocsCenterView.vue'),
|
||||||
},
|
},
|
||||||
{
|
|
||||||
path: 'packages',
|
|
||||||
component: () => import('@/views/billing/BillingView.vue'),
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
path: 'operation-logs',
|
path: 'operation-logs',
|
||||||
component: () => import('@/views/logs/OperationLogView.vue'),
|
component: () => import('@/views/logs/OperationLogView.vue'),
|
||||||
|
|||||||
@ -1,323 +0,0 @@
|
|||||||
<template>
|
|
||||||
<div>
|
|
||||||
<div class="page-header">
|
|
||||||
<div>
|
|
||||||
<h2>服务套餐</h2>
|
|
||||||
<p class="subtitle">查看当前租户已开通能力和配额使用情况。</p>
|
|
||||||
</div>
|
|
||||||
<el-button type="primary" plain @click="$router.push('/apps')">前往应用管理</el-button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 套餐卡片 -->
|
|
||||||
<el-row :gutter="16" style="margin-bottom: 16px">
|
|
||||||
<el-col :xs="24" :sm="8" v-for="plan in planCards" :key="plan.name">
|
|
||||||
<el-card
|
|
||||||
shadow="hover"
|
|
||||||
:class="['plan-card', plan.current ? 'plan-card-current' : '']"
|
|
||||||
>
|
|
||||||
<div class="plan-header">
|
|
||||||
<h3 class="plan-name">{{ plan.name }}</h3>
|
|
||||||
<el-tag v-if="plan.current" type="success" effect="dark">当前套餐</el-tag>
|
|
||||||
</div>
|
|
||||||
<div class="plan-price">
|
|
||||||
<span class="price-num">{{ plan.price }}</span>
|
|
||||||
<span class="price-unit">{{ plan.priceUnit }}</span>
|
|
||||||
</div>
|
|
||||||
<ul class="plan-features">
|
|
||||||
<li v-for="(f, idx) in plan.features" :key="idx">
|
|
||||||
<el-icon color="#67c23a"><CircleCheck /></el-icon>
|
|
||||||
<span>{{ f }}</span>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
<div class="plan-quota">
|
|
||||||
<div class="quota-row">
|
|
||||||
<span>应用上限</span>
|
|
||||||
<span class="quota-value">{{ plan.apps }}</span>
|
|
||||||
</div>
|
|
||||||
<div class="quota-row">
|
|
||||||
<span>子账号上限</span>
|
|
||||||
<span class="quota-value">{{ plan.subAccounts }}</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<el-button
|
|
||||||
v-if="!plan.current"
|
|
||||||
type="primary"
|
|
||||||
plain
|
|
||||||
class="plan-action"
|
|
||||||
@click="upgradePlan(plan.name)"
|
|
||||||
>
|
|
||||||
升级套餐
|
|
||||||
</el-button>
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
|
|
||||||
<!-- 用量统计 -->
|
|
||||||
<el-row :gutter="16" style="margin-bottom: 16px">
|
|
||||||
<el-col :xs="24" :sm="8">
|
|
||||||
<el-card shadow="hover">
|
|
||||||
<el-statistic title="应用数量" :value="summary.appCount" />
|
|
||||||
<div class="usage-bar">
|
|
||||||
<el-progress
|
|
||||||
:percentage="calcPercent(summary.appCount, currentPlan.appsNum)"
|
|
||||||
:status="calcStatus(summary.appCount, currentPlan.appsNum)"
|
|
||||||
/>
|
|
||||||
<div class="usage-text">
|
|
||||||
{{ summary.appCount }} / {{ currentPlan.apps }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
<el-col :xs="24" :sm="8">
|
|
||||||
<el-card shadow="hover">
|
|
||||||
<el-statistic title="已开通服务" :value="summary.serviceCount" />
|
|
||||||
<div class="usage-bar">
|
|
||||||
<el-progress :percentage="100" status="success" />
|
|
||||||
<div class="usage-text">全部可用</div>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
<el-col :xs="24" :sm="8">
|
|
||||||
<el-card shadow="hover">
|
|
||||||
<el-statistic title="子账号数量" :value="summary.subAccountCount" />
|
|
||||||
<div class="usage-bar">
|
|
||||||
<el-progress
|
|
||||||
:percentage="calcPercent(summary.subAccountCount, currentPlan.subAccountsNum)"
|
|
||||||
:status="calcStatus(summary.subAccountCount, currentPlan.subAccountsNum)"
|
|
||||||
/>
|
|
||||||
<div class="usage-text">
|
|
||||||
{{ summary.subAccountCount }} / {{ currentPlan.subAccounts }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</el-card>
|
|
||||||
</el-col>
|
|
||||||
</el-row>
|
|
||||||
|
|
||||||
<el-card style="margin-bottom: 16px">
|
|
||||||
<template #header>套餐与配额</template>
|
|
||||||
<el-table :data="plans" border stripe>
|
|
||||||
<el-table-column prop="name" label="套餐" width="160" />
|
|
||||||
<el-table-column prop="apps" label="应用上限" width="120" />
|
|
||||||
<el-table-column prop="subAccounts" label="子账号上限" width="120" />
|
|
||||||
<el-table-column prop="services" label="服务能力" min-width="320" />
|
|
||||||
</el-table>
|
|
||||||
</el-card>
|
|
||||||
|
|
||||||
<el-card>
|
|
||||||
<template #header>应用服务明细</template>
|
|
||||||
<el-table :data="appRows" v-loading="loading" border stripe>
|
|
||||||
<el-table-column prop="name" label="应用名称" min-width="160" />
|
|
||||||
<el-table-column prop="packageName" label="包名" min-width="180" />
|
|
||||||
<el-table-column prop="enabledServiceCount" label="已开通服务" width="120" />
|
|
||||||
<el-table-column prop="availableServices" label="服务类型" min-width="260" />
|
|
||||||
<el-table-column prop="createdAt" label="创建时间" width="180">
|
|
||||||
<template #default="{ row }">{{ fmt(row.createdAt) }}</template>
|
|
||||||
</el-table-column>
|
|
||||||
</el-table>
|
|
||||||
</el-card>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import { onMounted, reactive, ref, computed } from 'vue'
|
|
||||||
import { ElMessage } from 'element-plus'
|
|
||||||
import { CircleCheck } from '@element-plus/icons-vue'
|
|
||||||
import { appApi, type App } from '@/api/app'
|
|
||||||
import { dashboardApi } from '@/api/dashboard'
|
|
||||||
|
|
||||||
const loading = ref(false)
|
|
||||||
const apps = ref<App[]>([])
|
|
||||||
const summary = reactive({
|
|
||||||
appCount: 0,
|
|
||||||
serviceCount: 0,
|
|
||||||
subAccountCount: 0,
|
|
||||||
})
|
|
||||||
|
|
||||||
const planCards = [
|
|
||||||
{
|
|
||||||
name: '免费版',
|
|
||||||
price: '¥0',
|
|
||||||
priceUnit: '/月',
|
|
||||||
current: true,
|
|
||||||
apps: '3',
|
|
||||||
appsNum: 3,
|
|
||||||
subAccounts: '5',
|
|
||||||
subAccountsNum: 5,
|
|
||||||
features: ['IM 基础功能', 'Push 推送', 'Update 更新', '社区支持'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '专业版',
|
|
||||||
price: '¥299',
|
|
||||||
priceUnit: '/月',
|
|
||||||
current: false,
|
|
||||||
apps: '20',
|
|
||||||
appsNum: 20,
|
|
||||||
subAccounts: '20',
|
|
||||||
subAccountsNum: 20,
|
|
||||||
features: ['全部基础功能', 'Webhook 支持', '灰度发布', '优先客服'],
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: '企业版',
|
|
||||||
price: '¥999',
|
|
||||||
priceUnit: '/月',
|
|
||||||
current: false,
|
|
||||||
apps: '不限',
|
|
||||||
appsNum: Infinity,
|
|
||||||
subAccounts: '不限',
|
|
||||||
subAccountsNum: Infinity,
|
|
||||||
features: ['全部专业功能', '运营白名单', '专属客户经理', 'SLA 保障'],
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
const currentPlan = computed(() => planCards.find(p => p.current) || planCards[0])
|
|
||||||
|
|
||||||
const plans = [
|
|
||||||
{ name: '基础套餐', apps: '3', subAccounts: '5', services: 'IM / Push / Update 基础开通' },
|
|
||||||
{ name: '标准套餐', apps: '20', subAccounts: '20', services: 'IM / Push / Update + Webhook + 灰度' },
|
|
||||||
{ name: '企业套餐', apps: '不限', subAccounts: '不限', services: '全部能力 + 运营白名单' },
|
|
||||||
]
|
|
||||||
|
|
||||||
const appRows = ref<Array<App & { enabledServiceCount: number; availableServices: string }>>([])
|
|
||||||
|
|
||||||
async function loadData() {
|
|
||||||
loading.value = true
|
|
||||||
try {
|
|
||||||
const [dashRes, appsRes] = await Promise.all([dashboardApi.stats(), appApi.list()])
|
|
||||||
summary.appCount = dashRes.data.data.appCount
|
|
||||||
summary.serviceCount = dashRes.data.data.serviceCount
|
|
||||||
summary.subAccountCount = dashRes.data.data.subAccountCount
|
|
||||||
apps.value = appsRes.data.data
|
|
||||||
const serviceData = await Promise.all(apps.value.map(async (app) => {
|
|
||||||
const res = await appApi.getServices(app.id)
|
|
||||||
const services = res.data.data
|
|
||||||
return {
|
|
||||||
...app,
|
|
||||||
enabledServiceCount: services.filter(item => item.enabled).length,
|
|
||||||
availableServices: services.map(item => `${item.platform}/${item.serviceType}`).join(',') || '-',
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
appRows.value = serviceData
|
|
||||||
} finally {
|
|
||||||
loading.value = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function calcPercent(current: number, limit: number) {
|
|
||||||
if (limit === Infinity) return 0
|
|
||||||
if (limit <= 0) return 0
|
|
||||||
const p = Math.round((current / limit) * 100)
|
|
||||||
return p > 100 ? 100 : p
|
|
||||||
}
|
|
||||||
|
|
||||||
function calcStatus(current: number, limit: number) {
|
|
||||||
if (limit === Infinity) return undefined
|
|
||||||
const p = current / limit
|
|
||||||
if (p >= 1) return 'exception'
|
|
||||||
if (p >= 0.8) return 'warning'
|
|
||||||
return undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
function upgradePlan(name: string) {
|
|
||||||
ElMessage.info(`已选择「${name}」,请联系商务完成升级`)
|
|
||||||
}
|
|
||||||
|
|
||||||
function fmt(value: string) {
|
|
||||||
return value ? new Date(value).toLocaleString('zh-CN') : '-'
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(loadData)
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.page-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: flex-start;
|
|
||||||
gap: 16px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
.subtitle {
|
|
||||||
margin: 6px 0 0;
|
|
||||||
color: #606266;
|
|
||||||
}
|
|
||||||
|
|
||||||
.plan-card {
|
|
||||||
height: 100%;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
position: relative;
|
|
||||||
transition: transform 0.2s;
|
|
||||||
}
|
|
||||||
.plan-card:hover {
|
|
||||||
transform: translateY(-4px);
|
|
||||||
}
|
|
||||||
.plan-card-current {
|
|
||||||
border: 2px solid #67c23a;
|
|
||||||
}
|
|
||||||
.plan-header {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
margin-bottom: 12px;
|
|
||||||
}
|
|
||||||
.plan-name {
|
|
||||||
margin: 0;
|
|
||||||
font-size: 18px;
|
|
||||||
color: #303133;
|
|
||||||
}
|
|
||||||
.plan-price {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
.price-num {
|
|
||||||
font-size: 28px;
|
|
||||||
font-weight: 700;
|
|
||||||
color: #409eff;
|
|
||||||
}
|
|
||||||
.price-unit {
|
|
||||||
font-size: 14px;
|
|
||||||
color: #909399;
|
|
||||||
}
|
|
||||||
.plan-features {
|
|
||||||
list-style: none;
|
|
||||||
padding: 0;
|
|
||||||
margin: 0 0 16px;
|
|
||||||
flex: 1;
|
|
||||||
}
|
|
||||||
.plan-features li {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 6px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #606266;
|
|
||||||
}
|
|
||||||
.plan-quota {
|
|
||||||
margin-bottom: 12px;
|
|
||||||
padding-top: 12px;
|
|
||||||
border-top: 1px solid #ebeef5;
|
|
||||||
}
|
|
||||||
.quota-row {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #606266;
|
|
||||||
margin-bottom: 6px;
|
|
||||||
}
|
|
||||||
.quota-value {
|
|
||||||
font-weight: 600;
|
|
||||||
color: #303133;
|
|
||||||
}
|
|
||||||
.plan-action {
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
.usage-bar {
|
|
||||||
margin-top: 12px;
|
|
||||||
}
|
|
||||||
.usage-text {
|
|
||||||
margin-top: 6px;
|
|
||||||
font-size: 12px;
|
|
||||||
color: #909399;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
@ -88,7 +88,7 @@
|
|||||||
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
import { computed, onBeforeUnmount, onMounted, ref, watch } from 'vue'
|
||||||
import { useAuthStore } from '@/stores/auth'
|
import { useAuthStore } from '@/stores/auth'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { Coin, Document, Grid, Lock, Menu, Odometer, User } from '@element-plus/icons-vue'
|
import { Document, Grid, Lock, Menu, Odometer, User } from '@element-plus/icons-vue'
|
||||||
|
|
||||||
const auth = useAuthStore()
|
const auth = useAuthStore()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
@ -102,7 +102,6 @@ const navItems = computed(() => {
|
|||||||
{ path: '/apps', label: '我的应用', icon: Grid },
|
{ path: '/apps', label: '我的应用', icon: Grid },
|
||||||
{ path: '/security', label: '安全中心', icon: Lock },
|
{ path: '/security', label: '安全中心', icon: Lock },
|
||||||
{ path: '/docs', label: '接入文档', icon: Document },
|
{ path: '/docs', label: '接入文档', icon: Document },
|
||||||
{ path: '/packages', label: '服务套餐', icon: Coin },
|
|
||||||
{ path: '/operation-logs', label: '操作日志', icon: Document },
|
{ path: '/operation-logs', label: '操作日志', icon: Document },
|
||||||
]
|
]
|
||||||
if (auth.user?.type === 'MAIN') {
|
if (auth.user?.type === 'MAIN') {
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户