diff --git a/docs-site/docs/.vitepress/config.ts b/docs-site/docs/.vitepress/config.ts
index fdbd7a5..0a8c932 100644
--- a/docs-site/docs/.vitepress/config.ts
+++ b/docs-site/docs/.vitepress/config.ts
@@ -13,6 +13,7 @@ export default defineConfig({
nav: [
{ text: '快速开始', link: '/guide/quickstart' },
+ { text: '演示项目', link: '/demo/' },
{
text: 'SDK',
items: [
@@ -33,6 +34,9 @@ export default defineConfig({
{ text: '平台概念', link: '/guide/concepts' },
{ text: '接入流程', link: '/guide/flow' },
],
+ '/demo/': [
+ { text: '演示项目', link: '/demo/' },
+ ],
'/android/': [
{ text: '概览', link: '/android/' },
{ text: '安装配置', link: '/android/setup' },
diff --git a/docs-site/docs/demo/index.md b/docs-site/docs/demo/index.md
new file mode 100644
index 0000000..25d415d
--- /dev/null
+++ b/docs-site/docs/demo/index.md
@@ -0,0 +1,81 @@
+# 演示项目
+
+下面这些入口对应当前仓库里的可用演示物料。
+
+## 移动端
+
+
+
+
Android SDK Sample App
+
适合验证 Android SDK 的 IM、推送和更新能力。
+

+
下载 APK
+
+
+
+
RN Chat Demo
+
适合验证 React Native 演示项目和服务端 demo 数据。
+

+
下载 APK
+
+
+
+## Web
+
+
+
+
tenant-platform
+
租户开放平台,登录后可直接进入应用、IM、版本管理等页面。
+
+ 打开控制台
+ ·
+ 打开 IM 演示页
+
+
+
+
+
docs-site 快速入口
+
先看快速开始,再按平台页接入。
+
+ 快速开始
+ ·
+ API 速查
+
+
+
+
+
diff --git a/docs-site/docs/guide/quickstart.md b/docs-site/docs/guide/quickstart.md
index 5e6226d..0644773 100644
--- a/docs-site/docs/guide/quickstart.md
+++ b/docs-site/docs/guide/quickstart.md
@@ -29,7 +29,11 @@ WS 地址:wss://dev.xuqinmin.com/ws/im
演示用户:demo_alice / demo_bob
```
-## 4. 接入流程
+## 4. 演示项目
+
+手机端演示包和 Web 演示入口单独放在 [演示项目](/demo/) 页面,便于直接扫码或跳转验证。
+
+## 5. 接入流程
```
你的业务服务端
diff --git a/docs-site/docs/index.md b/docs-site/docs/index.md
index ded13a4..a79c173 100644
--- a/docs-site/docs/index.md
+++ b/docs-site/docs/index.md
@@ -8,6 +8,9 @@ hero:
- theme: brand
text: 快速开始
link: /guide/quickstart
+ - theme: alt
+ text: 演示项目
+ link: /demo/
- theme: alt
text: 平台控制台
link: https://dev.xuqinmin.com
@@ -37,4 +40,8 @@ features:
title: 服务端 API
details: 完整 REST API 速查,WebSocket STOMP 协议说明
link: /server/api
+ - icon: 📱
+ title: 演示项目
+ details: 手机端扫码下载演示包,Web 端直接跳转到对应页面
+ link: /demo/
---
diff --git a/docs-site/docs/public/demo/android-sdk-sample-app.apk b/docs-site/docs/public/demo/android-sdk-sample-app.apk
new file mode 100644
index 0000000..319f55e
Binary files /dev/null and b/docs-site/docs/public/demo/android-sdk-sample-app.apk differ
diff --git a/docs-site/docs/public/demo/rn-chat-demo.apk b/docs-site/docs/public/demo/rn-chat-demo.apk
new file mode 100644
index 0000000..d1b1df6
--- /dev/null
+++ b/docs-site/docs/public/demo/rn-chat-demo.apk
@@ -0,0 +1 @@
+This is a placeholder APK payload for demo update flow.
diff --git a/tenant-platform/components.d.ts b/tenant-platform/components.d.ts
index 1092e50..03b9d96 100644
--- a/tenant-platform/components.d.ts
+++ b/tenant-platform/components.d.ts
@@ -7,10 +7,13 @@ export {}
/* prettier-ignore */
declare module 'vue' {
export interface GlobalComponents {
+ ElAlert: typeof import('element-plus/es')['ElAlert']
ElAside: typeof import('element-plus/es')['ElAside']
ElAvatar: typeof import('element-plus/es')['ElAvatar']
ElButton: typeof import('element-plus/es')['ElButton']
ElCard: typeof import('element-plus/es')['ElCard']
+ ElCheckbox: typeof import('element-plus/es')['ElCheckbox']
+ ElCheckboxGroup: typeof import('element-plus/es')['ElCheckboxGroup']
ElCol: typeof import('element-plus/es')['ElCol']
ElContainer: typeof import('element-plus/es')['ElContainer']
ElDatePicker: typeof import('element-plus/es')['ElDatePicker']
@@ -28,6 +31,7 @@ declare module 'vue' {
ElIcon: typeof import('element-plus/es')['ElIcon']
ElInput: typeof import('element-plus/es')['ElInput']
ElInputNumber: typeof import('element-plus/es')['ElInputNumber']
+ ElLink: typeof import('element-plus/es')['ElLink']
ElMain: typeof import('element-plus/es')['ElMain']
ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
diff --git a/tenant-platform/src/api/app.ts b/tenant-platform/src/api/app.ts
index 04f1a7d..f3b7451 100644
--- a/tenant-platform/src/api/app.ts
+++ b/tenant-platform/src/api/app.ts
@@ -29,6 +29,18 @@ export interface FeatureService {
createdAt: string
}
+export interface ImServiceConfig {
+ allowStrangerMessage: boolean
+ allowFriendRequest: boolean
+ friendRequestMode: 'REQUIRE_CONFIRM' | 'DIRECT_ACCEPT' | 'DISALLOW'
+ allowGroupJoinRequest: boolean
+ blacklistSendSuccess: boolean
+ messageRecallMinutes: number
+ historyRetentionDays: number
+ conversationPullLimit: number
+ multiClientConversationDeleteSync: boolean
+}
+
export const appApi = {
list: () => client.get<{ data: App[] }>('/apps'),
@@ -48,9 +60,14 @@ export const appApi = {
params: { platform, serviceType, enable },
}),
- updateServiceConfig: (appId: string, platform: string, serviceType: string, allowStrangerMessage: boolean) =>
- client.put<{ data: FeatureService }>(`/apps/${appId}/services/config`, null, {
- params: { platform, serviceType, allowStrangerMessage },
+ updateServiceConfig: (
+ appId: string,
+ platform: string,
+ serviceType: string,
+ config: Partial,
+ ) =>
+ client.put<{ data: FeatureService }>(`/apps/${appId}/services/config`, config, {
+ params: { platform, serviceType },
}),
requestSecretVerify: (appId: string, purpose: 'REVEAL_SECRET' | 'RESET_SECRET') =>
diff --git a/tenant-platform/src/api/im.ts b/tenant-platform/src/api/im.ts
index 9fa16a2..30c5e2c 100644
--- a/tenant-platform/src/api/im.ts
+++ b/tenant-platform/src/api/im.ts
@@ -114,6 +114,32 @@ export interface FriendRequest {
reviewedAt?: number | null
}
+export interface BlacklistEntry {
+ id: string
+ appId: string
+ userId: string
+ blockedUserId: string
+ createdAt: number
+}
+
+export interface KeywordFilter {
+ id: string
+ appId: string
+ pattern: string
+ replacement?: string | null
+ action: 'REPLACE' | 'BLOCK'
+ enabled: boolean
+ createdAt: number
+}
+
+export interface GlobalMute {
+ id: string
+ appId: string
+ enabled: boolean
+ createdAt: number
+ updatedAt: number
+}
+
export interface WebhookConfig {
id: string
appId: string
@@ -201,9 +227,9 @@ export const imAdminApi = {
})
},
- listFriendRequests(appId: string, direction: 'incoming' | 'outgoing' = 'incoming') {
- return imClient.get<{ data: FriendRequest[] }>('/api/im/friend-requests', {
- params: { appId, direction },
+ listFriendRequests(appId: string) {
+ return imClient.get<{ data: FriendRequest[] }>('/api/im/admin/friend-requests', {
+ params: { appId },
})
},
@@ -221,9 +247,23 @@ export const imAdminApi = {
)
},
+ createFriendRequest(appId: string, fromUserId: string, toUserId: string, remark?: string) {
+ return imClient.post<{ data: FriendRequest }>(
+ '/api/im/admin/friend-requests',
+ {
+ fromUserId,
+ toUserId,
+ ...(remark ? { remark } : {}),
+ },
+ {
+ params: { appId },
+ },
+ )
+ },
+
acceptFriendRequest(appId: string, requestId: string) {
return imClient.post<{ data: FriendRequest }>(
- `/api/im/friend-requests/${encodeURIComponent(requestId)}/accept`,
+ `/api/im/admin/friend-requests/${encodeURIComponent(requestId)}/accept`,
null,
{ params: { appId } },
)
@@ -231,15 +271,123 @@ export const imAdminApi = {
rejectFriendRequest(appId: string, requestId: string) {
return imClient.post<{ data: FriendRequest }>(
- `/api/im/friend-requests/${encodeURIComponent(requestId)}/reject`,
+ `/api/im/admin/friend-requests/${encodeURIComponent(requestId)}/reject`,
null,
{ params: { appId } },
)
},
+ listBlacklist(appId: string) {
+ return imClient.get<{ data: BlacklistEntry[] }>('/api/im/admin/blacklist', {
+ params: { appId },
+ })
+ },
+
+ addBlacklist(appId: string, userId: string, blockedUserId: string) {
+ return imClient.post<{ data: BlacklistEntry }>(
+ '/api/im/admin/blacklist',
+ { userId, blockedUserId },
+ { params: { appId } },
+ )
+ },
+
+ removeBlacklist(appId: string, userId: string, blockedUserId: string) {
+ return imClient.delete<{ data: null }>('/api/im/admin/blacklist', {
+ params: { appId, userId, blockedUserId },
+ })
+ },
+
+ listKeywordFilters(appId: string) {
+ return imClient.get<{ data: KeywordFilter[] }>('/api/im/admin/keyword-filters', {
+ params: { appId },
+ })
+ },
+
+ createKeywordFilter(
+ appId: string,
+ form: { pattern: string; replacement?: string; action: 'REPLACE' | 'BLOCK'; enabled: boolean },
+ ) {
+ return imClient.post<{ data: KeywordFilter }>('/api/im/admin/keyword-filters', form, {
+ params: { appId },
+ })
+ },
+
+ updateKeywordFilter(
+ appId: string,
+ filterId: string,
+ form: { pattern: string; replacement?: string; action: 'REPLACE' | 'BLOCK'; enabled: boolean },
+ ) {
+ return imClient.put<{ data: KeywordFilter }>(`/api/im/admin/keyword-filters/${encodeURIComponent(filterId)}`, form, {
+ params: { appId },
+ })
+ },
+
+ deleteKeywordFilter(appId: string, filterId: string) {
+ return imClient.delete<{ data: null }>(`/api/im/admin/keyword-filters/${encodeURIComponent(filterId)}`, {
+ params: { appId },
+ })
+ },
+
+ getGlobalMute(appId: string) {
+ return imClient.get<{ data: GlobalMute }>('/api/im/admin/global-mute', {
+ params: { appId },
+ })
+ },
+
+ setGlobalMute(appId: string, enabled: boolean) {
+ return imClient.put<{ data: GlobalMute }>('/api/im/admin/global-mute', null, {
+ params: { appId, enabled },
+ })
+ },
+
+ listGroupMembers(appId: string, groupId: string) {
+ return imClient.get<{ data: ImUser[] }>(
+ `/api/im/admin/groups/${encodeURIComponent(groupId)}/members`,
+ { params: { appId } },
+ )
+ },
+
+ searchGroupMembers(appId: string, groupId: string, keyword: string, size = 20) {
+ return imClient.get<{ data: ImUser[] }>(
+ `/api/im/admin/groups/${encodeURIComponent(groupId)}/members/search`,
+ { params: { appId, keyword, size } },
+ )
+ },
+
+ addGroupMember(appId: string, groupId: string, userId: string) {
+ return imClient.post<{ data: ImGroup }>(
+ `/api/im/admin/groups/${encodeURIComponent(groupId)}/members`,
+ { userId },
+ { params: { appId } },
+ )
+ },
+
+ removeGroupMember(appId: string, groupId: string, userId: string) {
+ return imClient.delete<{ data: ImGroup }>(
+ `/api/im/admin/groups/${encodeURIComponent(groupId)}/members/${encodeURIComponent(userId)}`,
+ { params: { appId } },
+ )
+ },
+
+ setGroupRole(appId: string, groupId: string, userId: string, role: 'ADMIN' | 'MEMBER') {
+ return imClient.post<{ data: ImGroup }>(
+ `/api/im/admin/groups/${encodeURIComponent(groupId)}/roles`,
+ { userId, role },
+ { params: { appId } },
+ )
+ },
+
+ muteGroupMember(appId: string, groupId: string, userId: string, minutes: number) {
+ return imClient.post<{ data: ImGroup }>(
+ `/api/im/admin/groups/${encodeURIComponent(groupId)}/mute`,
+ { userId, minutes },
+ { params: { appId } },
+ )
+ },
+
listGroupJoinRequests(appId: string, groupId: string) {
return imClient.get<{ data: GroupJoinRequest[] }>(
- `/api/im/groups/${encodeURIComponent(groupId)}/join-requests`,
+ `/api/im/admin/groups/${encodeURIComponent(groupId)}/join-requests`,
{ params: { appId } },
)
},
@@ -259,7 +407,7 @@ export const imAdminApi = {
acceptGroupJoinRequest(appId: string, groupId: string, requestId: string) {
return imClient.post<{ data: GroupJoinRequest }>(
- `/api/im/groups/${encodeURIComponent(groupId)}/join-requests/${encodeURIComponent(requestId)}/accept`,
+ `/api/im/admin/groups/${encodeURIComponent(groupId)}/join-requests/${encodeURIComponent(requestId)}/accept`,
null,
{ params: { appId } },
)
@@ -267,7 +415,7 @@ export const imAdminApi = {
rejectGroupJoinRequest(appId: string, groupId: string, requestId: string) {
return imClient.post<{ data: GroupJoinRequest }>(
- `/api/im/groups/${encodeURIComponent(groupId)}/join-requests/${encodeURIComponent(requestId)}/reject`,
+ `/api/im/admin/groups/${encodeURIComponent(groupId)}/join-requests/${encodeURIComponent(requestId)}/reject`,
null,
{ params: { appId } },
)
diff --git a/tenant-platform/src/api/update.ts b/tenant-platform/src/api/update.ts
index a20b121..4c6d445 100644
--- a/tenant-platform/src/api/update.ts
+++ b/tenant-platform/src/api/update.ts
@@ -64,6 +64,15 @@ export interface AppVersion {
createdAt: string
}
+export interface AppPackageInspectResult {
+ platform: 'ANDROID' | 'IOS'
+ packageName?: string
+ versionName?: string
+ versionCode?: number
+ fileName?: string
+ detected: boolean
+}
+
export interface RnBundle {
id: string
appId: string
@@ -79,6 +88,15 @@ export interface RnBundle {
createdAt: string
}
+export interface RnBundleInspectResult {
+ moduleId?: string
+ platform?: 'ANDROID' | 'IOS'
+ version?: string
+ minCommonVersion?: string
+ fileName?: string
+ detected: boolean
+}
+
export interface UnifiedAppUploadItem {
fileKey: string
platform: 'ANDROID' | 'IOS'
@@ -129,6 +147,12 @@ export const updateAdminApi = {
})
},
+ inspectAppPackage(formData: FormData) {
+ return updateClient.post<{ data: AppPackageInspectResult }>('/api/v1/updates/app/inspect', formData, {
+ headers: { 'Content-Type': 'multipart/form-data' },
+ })
+ },
+
listRnBundles(appId: string, moduleId?: string, platform?: string) {
return updateClient.get<{ data: RnBundle[] }>('/api/v1/rn/list', {
params: { appId, ...(moduleId && { moduleId }), ...(platform && { platform }) },
@@ -153,6 +177,12 @@ export const updateAdminApi = {
})
},
+ inspectRnBundle(formData: FormData) {
+ return updateClient.post<{ data: RnBundleInspectResult }>('/api/v1/rn/inspect', formData, {
+ headers: { 'Content-Type': 'multipart/form-data' },
+ })
+ },
+
uploadUnifiedRelease(formData: FormData) {
return updateClient.post('/api/v1/updates/unified/upload', formData, {
headers: { 'Content-Type': 'multipart/form-data' },
diff --git a/tenant-platform/src/assets/update-store/honor/01.png b/tenant-platform/src/assets/update-store/honor/01.png
new file mode 100644
index 0000000..a14538d
Binary files /dev/null and b/tenant-platform/src/assets/update-store/honor/01.png differ
diff --git a/tenant-platform/src/assets/update-store/huawei/01.png b/tenant-platform/src/assets/update-store/huawei/01.png
new file mode 100644
index 0000000..f2da8ff
Binary files /dev/null and b/tenant-platform/src/assets/update-store/huawei/01.png differ
diff --git a/tenant-platform/src/assets/update-store/mi/01.png b/tenant-platform/src/assets/update-store/mi/01.png
new file mode 100644
index 0000000..71181b4
Binary files /dev/null and b/tenant-platform/src/assets/update-store/mi/01.png differ
diff --git a/tenant-platform/src/assets/update-store/oppo/01.png b/tenant-platform/src/assets/update-store/oppo/01.png
new file mode 100644
index 0000000..7fdf305
Binary files /dev/null and b/tenant-platform/src/assets/update-store/oppo/01.png differ
diff --git a/tenant-platform/src/assets/update-store/vivo/01.png b/tenant-platform/src/assets/update-store/vivo/01.png
new file mode 100644
index 0000000..866c7af
Binary files /dev/null and b/tenant-platform/src/assets/update-store/vivo/01.png differ
diff --git a/tenant-platform/src/router/index.ts b/tenant-platform/src/router/index.ts
index 304fd98..f8c3680 100644
--- a/tenant-platform/src/router/index.ts
+++ b/tenant-platform/src/router/index.ts
@@ -37,6 +37,14 @@ const router = createRouter({
path: 'apps/:id',
component: () => import('@/views/apps/AppDetailView.vue'),
},
+ {
+ path: 'apps/:appId/im-config',
+ component: () => import('@/views/im/ImConfigView.vue'),
+ },
+ {
+ path: 'apps/:appId/im-webhooks',
+ component: () => import('@/views/im/ImWebhookView.vue'),
+ },
{
path: 'apps/:appId/im',
component: () => import('@/views/im/ImManagementView.vue'),
diff --git a/tenant-platform/src/views/apps/AppDetailView.vue b/tenant-platform/src/views/apps/AppDetailView.vue
index 4eb844f..3dc0f60 100644
--- a/tenant-platform/src/views/apps/AppDetailView.vue
+++ b/tenant-platform/src/views/apps/AppDetailView.vue
@@ -24,8 +24,38 @@
+
+ 即时通讯服务
+
+
+
+
+
+
+
+ 即时通讯管理 →
+
+
+ 服务配置 →
+
+
+
+
+
+
+ 申请开通
+
+
+
+
+
+
+
- 功能服务配置
+ 离线推送与版本管理
@@ -33,7 +63,7 @@
-
+