docs: 添加 SDK API 重设计、安全设计规范和测试进度跟踪文档

- 新增 SDK API 重设计规范文档,统一各端 SDK 初始化、登录、消息接口
- 新增安全设计规范文档,涵盖密码安全、AppSecret 验证、令牌存储等安全要点
- 新增 Bug 跟踪记录文档,记录已修复问题和开放问题
- 新增测试进度跟踪文档,记录各模块测试覆盖情况和验证结果
这个提交包含在:
XuqmGroup 2026-05-02 11:45:43 +08:00
父节点 e5552044ae
当前提交 af253f688a
共有 8 个文件被更改,包括 8 次插入338 次删除

查看文件

@ -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: 本地消息存储在哪里?**
使用 WatermelonDBSQLite,按 `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') {