docs: 添加 SDK API 重设计、安全设计规范和测试进度跟踪文档
- 新增 SDK API 重设计规范文档,统一各端 SDK 初始化、登录、消息接口 - 新增安全设计规范文档,涵盖密码安全、AppSecret 验证、令牌存储等安全要点 - 新增 Bug 跟踪记录文档,记录已修复问题和开放问题 - 新增测试进度跟踪文档,记录各模块测试覆盖情况和验证结果
这个提交包含在:
父节点
e5552044ae
当前提交
af253f688a
@ -44,7 +44,6 @@ yarn workspace ops-platform dev
|
||||
| `/apps/:id` | AppDetailView | 应用详情(需登录) |
|
||||
| `/security` | SecurityCenterView | 安全中心(需登录) |
|
||||
| `/docs` | DocsCenterView | 接入文档(需登录) |
|
||||
| `/packages` | BillingView | 服务套餐 / 配额(需登录) |
|
||||
| `/accounts` | SubAccountView | 子账号管理(需登录) |
|
||||
|
||||
### 认证流程
|
||||
@ -94,11 +93,10 @@ JWT Payload 由 `atob(token.split('.')[1])` 解析,无需额外请求。
|
||||
- 每个平台下显示 `IM` / `推送` / `版本管理` 三个服务卡片
|
||||
- 支持一键开关服务、复制 secretKey、重新生成 secretKey
|
||||
|
||||
### 安全中心 / 接入文档 / 服务套餐
|
||||
### 安全中心 / 接入文档
|
||||
|
||||
- 安全中心提供 AppSecret 查看/重置入口,直接复用邮箱验证码流程
|
||||
- 接入文档页提供 RN、Android / iOS 和服务端的最短接入示例
|
||||
- 服务套餐页展示当前租户的配额和服务开通概览,不涉及费用计费
|
||||
|
||||
### 子账号管理(SubAccountView)
|
||||
|
||||
|
||||
@ -52,7 +52,7 @@ XuqmSDK.initialize(
|
||||
|
||||
```kotlin
|
||||
// 登录(协程 suspend 函数)
|
||||
// 只需要 userId + userSig,不需要 nickname / avatar / 过期字段
|
||||
// 只需要 userId + userSig,不需要 nickname / avatar / 生命周期字段
|
||||
lifecycleScope.launch {
|
||||
XuqmSDK.login(
|
||||
userId = "user_001",
|
||||
@ -160,7 +160,7 @@ XuqmSDK.logout()
|
||||
// ↓ 自动触发所有模块清理
|
||||
```
|
||||
|
||||
> **注意**:`userSig` 由业务服务端签发,SDK 侧不做续签。若需更新登录态,请直接重新登录;如需撤销权限,可在租户平台重置 `appSecret` 或拉黑账号。
|
||||
> **注意**:`userSig` 由业务服务端签发。若需更新登录态,请直接重新登录;如需撤销权限,可在租户平台重置 `appSecret` 或拉黑账号。
|
||||
|
||||
### 8. 版本更新
|
||||
|
||||
|
||||
@ -62,7 +62,7 @@ XuqmSDK.shared.initialize(config: config)
|
||||
import XuqmIM
|
||||
|
||||
// 使用 UserSig 登录(推荐生产环境)
|
||||
// 只需要 userId + userSig,不需要 nickname / avatar / 过期字段
|
||||
// 只需要 userId + userSig,不需要 nickname / avatar / 生命周期字段
|
||||
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()
|
||||
```
|
||||
|
||||
> **注意**:`userSig` 由业务服务端签发,SDK 侧不做续签。若需更新登录态,请直接重新登录。
|
||||
> **注意**:`userSig` 由业务服务端签发。若需更新登录态,请直接重新登录。
|
||||
|
||||
### 10. 检查更新
|
||||
|
||||
|
||||
@ -280,7 +280,7 @@ UserSig 生成方式见 [安全设计文档](../../design/02-security-design.md)
|
||||
XuqmGroup 是托管平台,服务地址统一管理,与腾讯云 IM 等平台的设计一致,开发者只需关心业务逻辑。
|
||||
|
||||
**Q: UserSig 是什么?**
|
||||
UserSig 是您的业务服务端用 AppSecret 为用户签发的安全凭证,有效期可配置。AppSecret 绝不下发到客户端。
|
||||
UserSig 是您的业务服务端用 AppSecret 为用户签发的安全凭证。当前项目的 IM 登录不做过期功能,只校验 `userId + UserSig` 是否匹配。AppSecret 绝不下发到客户端。
|
||||
|
||||
**Q: 本地消息存储在哪里?**
|
||||
使用 WatermelonDB(SQLite),按 `appKey + userId` 自动隔离,多账号切换安全。
|
||||
|
||||
@ -50,7 +50,7 @@ POST /api/im/auth/login
|
||||
{ "token": "eyJ..." }
|
||||
```
|
||||
|
||||
> SDK 侧不做续签;登录态更新由业务侧重新登录完成。
|
||||
> 登录态更新由业务侧重新登录完成。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -41,10 +41,6 @@ const router = createRouter({
|
||||
path: 'docs',
|
||||
component: () => import('@/views/docs/DocsCenterView.vue'),
|
||||
},
|
||||
{
|
||||
path: 'packages',
|
||||
component: () => import('@/views/billing/BillingView.vue'),
|
||||
},
|
||||
{
|
||||
path: 'operation-logs',
|
||||
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 { useAuthStore } from '@/stores/auth'
|
||||
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 route = useRoute()
|
||||
@ -102,7 +102,6 @@ const navItems = computed(() => {
|
||||
{ path: '/apps', label: '我的应用', icon: Grid },
|
||||
{ path: '/security', label: '安全中心', icon: Lock },
|
||||
{ path: '/docs', label: '接入文档', icon: Document },
|
||||
{ path: '/packages', label: '服务套餐', icon: Coin },
|
||||
{ path: '/operation-logs', label: '操作日志', icon: Document },
|
||||
]
|
||||
if (auth.user?.type === 'MAIN') {
|
||||
|
||||
正在加载...
在新工单中引用
屏蔽一个用户