diff --git a/README.md b/README.md index a734a49..76a1e03 100644 --- a/README.md +++ b/README.md @@ -42,6 +42,9 @@ yarn workspace ops-platform dev | `/dashboard` | DashboardView | 控制台首页(需登录) | | `/apps` | AppListView | 应用列表(需登录) | | `/apps/:id` | AppDetailView | 应用详情(需登录) | +| `/security` | SecurityCenterView | 安全中心(需登录) | +| `/docs` | DocsCenterView | 接入文档(需登录) | +| `/packages` | BillingView | 服务套餐 / 配额(需登录) | | `/accounts` | SubAccountView | 子账号管理(需登录) | ### 认证流程 @@ -91,6 +94,12 @@ JWT Payload 由 `atob(token.split('.')[1])` 解析,无需额外请求。 - 每个平台下显示 `IM` / `推送` / `版本管理` 三个服务卡片 - 支持一键开关服务、复制 secretKey、重新生成 secretKey +### 安全中心 / 接入文档 / 服务套餐 + +- 安全中心提供 AppSecret 查看/重置入口,直接复用邮箱验证码流程 +- 接入文档页提供 RN、Android / iOS 和服务端的最短接入示例 +- 服务套餐页展示当前租户的配额和服务开通概览,不涉及费用计费 + ### 子账号管理(SubAccountView) 创建子账号**两步流程**: @@ -109,7 +118,12 @@ JWT Payload 由 `atob(token.split('.')[1])` 解析,无需额外请求。 |------|------|------| | `/login` | LoginView | 运营管理员登录 | | `/tenants` | TenantListView | 租户列表(搜索/分页/启用禁用) | -| `/statistics` | StatisticsView | 数据统计 | +| `/tenants/:id` | TenantDetailView | 租户详情 | +| `/apps` | AppListView | 应用列表 | +| `/apps/:id` | AppDetailView | 应用详情 | +| `/statistics` | StatisticsView | 服务运营总览 | +| `/service-requests` | ServiceRequestsView | 服务开通审核 | +| `/operation-logs` | OperationLogView | 操作日志 | ### 认证 diff --git a/docs-site/docs/android/index.md b/docs-site/docs/android/index.md index a68e5c4..287b51b 100644 --- a/docs-site/docs/android/index.md +++ b/docs-site/docs/android/index.md @@ -1,17 +1,15 @@ # Android SDK 接入指南 -**版本**:0.2.x(v0.4.0 将引入 UserSig 鉴权)· **最低 Android 版本**:API 24 (Android 7.0) · **语言**:Kotlin - -> **注意**:v0.4.0 将是 Breaking 版本,`initialize()` 将保持不变,`ImSDK.login()` 将改为 UserSig 鉴权模式。 +**版本**:0.4.x(UserSig 鉴权)· **最低 Android 版本**:API 24 (Android 7.0) · **语言**:Kotlin ## 功能模块 | 模块 | Artifact | 功能 | |------|----------|------| -| sdk-core | `com.xuqm:sdk-core` | 初始化、网络、鉴权(EncryptedSharedPreferences)| +| sdk-core | `com.xuqm:sdk-core` | 初始化、网络、鉴权、UserSig 续签 | | sdk-im | `com.xuqm:sdk-im` | 单聊、群聊、消息收发、会话、好友、群组 | -| sdk-push | `com.xuqm:sdk-push` | 自动检测厂商、设备 Token 注册 | -| sdk-update | `com.xuqm:sdk-update` | App 更新检查、RN 热更新 | +| sdk-push | `com.xuqm:sdk-push` | 自动检测厂商、设备 Token 注册(华为/小米/OPPO/vivo/荣耀/FCM) | +| sdk-update | `com.xuqm:sdk-update` | App 更新检查、下载安装 | ## 快速接入 @@ -30,10 +28,10 @@ dependencyResolutionManagement { ```kotlin // app/build.gradle.kts dependencies { - implementation("com.xuqm:sdk-core:0.2.0") - implementation("com.xuqm:sdk-im:0.2.0") - implementation("com.xuqm:sdk-push:0.2.0") // 按需 - implementation("com.xuqm:sdk-update:0.2.0") // 按需 + implementation("com.xuqm:sdk-core:0.4.0") + implementation("com.xuqm:sdk-im:0.4.0") + implementation("com.xuqm:sdk-push:0.4.0") // 按需 + implementation("com.xuqm:sdk-update:0.4.0") // 按需 } ``` @@ -50,12 +48,18 @@ XuqmSDK.initialize( ) ``` -### 3. IM 登录与收消息 +### 3. IM 登录与收消息(UserSig 模式) ```kotlin // 登录(协程 suspend 函数) lifecycleScope.launch { - ImSDK.login(userId = "user_001", nickname = "张三") + XuqmSDK.login( + userId = "user_001", + userSig = "your_user_sig_jwt", // 由业务服务端签发 + nickname = "张三", + avatar = "https://...", + userSigExpiresAt = 1893456000000L, // UserSig 过期时间戳(毫秒),可选 + ) } // 监听实时消息 @@ -63,19 +67,30 @@ ImSDK.addListener(object : ImEventListener { override fun onConnected() { /* WebSocket 已连接 */ } override fun onMessage(msg: ImMessage) { /* 单聊消息 */ } override fun onGroupMessage(msg: ImMessage) { /* 群聊消息 */ } + override fun onRead(msg: ImMessage) { /* 对方已读回执 */ } + override fun onRevoke(msg: ImMessage) { /* 消息被撤回 */ } override fun onDisconnected(reason: String?) { /* 断线处理 */ } + override fun onError(error: String) { /* 错误回调 */ } }) ``` ### 4. 发送消息 ```kotlin -// 发送文本(通过 WebSocket 实时发送) -ImSDK.sendMessage( +// 发送文本 +ImSDK.sendTextMessage( toId = "user_002", chatType = "SINGLE", - msgType = "TEXT", - content = """{"text":"Hello!"}""", + content = "Hello!", +) + +// 发送图片(需先调用 FileSDK 上传) +ImSDK.sendImageMessage( + toId = "user_002", + chatType = "SINGLE", + file = uploadResult, + width = 800, + height = 600, ) ``` @@ -84,7 +99,7 @@ ImSDK.sendMessage( ```kotlin // 群组 val groups = ImSDK.listGroups() -val group = ImSDK.createGroup("项目讨论", listOf("user_002", "user_003")) +val group = ImSDK.createGroup("项目讨论", listOf("user_002", "user_003")) ImSDK.addGroupMember(groupId, "user_004") ImSDK.leaveGroup(groupId) @@ -95,23 +110,88 @@ ImSDK.addFriend("user_002") // 会话 val conversations = ImSDK.listConversations() ImSDK.setConversationPinned(targetId, "SINGLE", true) -ImSDK.markRead(targetId) +ImSDK.setConversationMuted(targetId, "SINGLE", true) +ImSDK.markRead(targetId, "SINGLE") +ImSDK.setDraft(targetId, "SINGLE", "草稿内容") +ImSDK.deleteConversation(targetId, "SINGLE") ``` -### 6. 推送设备注册 +### 6. Push 多厂商接入 + +SDK 在登录后会自动检测手机厂商并初始化对应 Push 服务。业务层通常无需手动调用,但可根据需要主动控制: ```kotlin -// 登录后调用;SDK 自动检测手机厂商(华为/小米/OPPO/vivo/荣耀) -PushSDK.registerDevice(context, userId = "user_001") +// 手动检测当前设备厂商 +val vendor = PushSDK.detectVendor() +// 返回:HUAWEI / XIAOMI / OPPO / VIVO / HONOR / FCM + +// 手动初始化各厂商 Push SDK(通常由 onSdkLogin 自动调用) +PushSDK.initializeVendors(context) + +// 手动绑定/解绑 IM 用户与 Push Token +PushSDK.bindImUser(context, userId = "user_001") +PushSDK.unbindImUser(userId = "user_001") + +// 开启或关闭推送接收 +PushSDK.setReceivePush(context, enabled = false) ``` -### 7. 版本更新 +**各厂商接入注意事项**: + +| 厂商 | 需额外依赖 | 说明 | +|------|-----------|------| +| 华为 | `com.huawei.hms:push` | 需在华为开发者平台配置 AppID | +| 小米 | `com.xiaomi.mipush:mipush-sdk` | 需在小米开放平台申请 AppID/AppKey | +| OPPO | `com.heytap.mcssdk:mcssdk` | 需在 OPPO 开放平台申请 | +| vivo | `com.vivo.push:vivopush` | 需在 vivo 开放平台申请 | +| 荣耀 | `com.hihonor.mcs:mcs-push` | 需在荣耀开发者服务平台配置 | +| FCM | `com.google.firebase:firebase-messaging` | 模拟器无 Google Play 服务时会 fallback | + +### 7. UserSig 续签 + +在 `UserSig` 即将过期(默认提前 5 分钟)时,SDK 会触发续签回调。业务层应在回调中向自己的服务端申请新的 UserSig,然后重新调用 `XuqmSDK.login()` 刷新 Token 和定时器。 + +```kotlin +// 设置续签监听器(建议在初始化后、登录前设置) +XuqmSDK.setUserSigRefreshListener { + // 异步获取新 UserSig + lifecycleScope.launch { + val newUserSig = yourBackend.refreshUserSig("user_001") + XuqmSDK.login( + userId = "user_001", + userSig = newUserSig, + userSigExpiresAt = newExpiryTimeMs, + ) + } +} +``` + +> **注意**:`userSigExpiresAt` 为 Unix 时间戳(毫秒)。若登录时不传该值,则不会启动续签定时器。 + +### 8. 版本更新 ```kotlin // 检查 App 更新 val update = UpdateSDK.checkAppUpdate(context) if (update?.needsUpdate == true) { - UpdateSDK.downloadAndInstall(context, update.downloadUrl) + UpdateSDK.downloadAndInstall(context, update.downloadUrl) { progress -> + // 更新下载进度 0-100 + } +} +``` + +### 9. 连接状态监听 + +```kotlin +// 通过 StateFlow 监听 IM 连接状态 +lifecycleScope.launch { + ImSDK.connectionState.collect { state -> + when (state) { + is ImConnectionState.Connected -> { /* 已连接 */ } + is ImConnectionState.Connecting -> { /* 连接中 */ } + is ImConnectionState.Disconnected -> { /* 已断开:state.reason */ } + } + } } ``` diff --git a/docs-site/docs/guide/quickstart.md b/docs-site/docs/guide/quickstart.md index a29c428..f491d47 100644 --- a/docs-site/docs/guide/quickstart.md +++ b/docs-site/docs/guide/quickstart.md @@ -6,7 +6,7 @@ 1. 访问 [XuqmGroup 控制台](https://dev.xuqinmin.com) 2. 注册租户账号,创建应用 -3. 记录 `appKey` +3. 记录 `appKey`(Android)或 `appId + appSecret`(iOS) ## 2. 选择你的平台 @@ -38,14 +38,85 @@ WS 地址:wss://dev.xuqinmin.com/ws/im ``` 你的业务服务端 → 持有 appKey/appSecret - → 调用 IM 登录接口换取 IM Token + → 调用 IM 登录接口换取 IM Token(或签发 UserSig JWT) → 平台内部协议字段由 SDK 和后端自动处理,业务方无需感知 - → 返回 Token 给客户端 + → 返回 Token / UserSig 给客户端 客户端 SDK - → 使用 Token 初始化 IM 连接 + → 使用 Token / UserSig 初始化 IM 连接 → 建立 WebSocket 长连接 → 开始收发消息 ``` > **安全提示**:appSecret 应仅在你的服务端持有,不应下发给客户端。 + +--- + +## 6. Android Demo 运行说明 + +### 环境要求 + +- Android Studio Ladybug(2024.2.1)或更高版本 +- JDK 21 +- Android 模拟器或真机(API 24+) + +### 运行步骤 + +1. 打开 `XuqmGroup-AndroidSDK` 目录为 Android Studio 项目 +2. 等待 Gradle Sync 完成(首次可能需要下载依赖) +3. 在 `sample-app/src/main/java/.../MainActivity.kt` 或对应配置中修改服务器地址(如需连接本地环境) +4. 选择模拟器或真机,点击 **Run 'sample-app'** +5. 使用演示账号登录:`user_a` / `123456` 或 `user_b` / `123456` + +### 关键路径 + +``` +APK 输出:XuqmGroup-AndroidSDK/sample-app/build/outputs/apk/debug/sample-app-debug.apk +主 Activity:com.xuqm.sdk.sample.MainActivity +包名:com.xuqm.demo +``` + +### 常用命令 + +```bash +# 构建全量 SDK + App +cd XuqmGroup-AndroidSDK && ./gradlew clean build + +# 安装到指定设备 +adb -s emulator-5556 install -r sample-app/build/outputs/apk/debug/sample-app-debug.apk + +# 查看 IM 日志 +adb -s emulator-5556 logcat -d "*:S" XuqmImSDK:D XuqmImClient:D 2>/dev/null | tail -30 +``` + +--- + +## 7. iOS Demo 运行说明 + +### 环境要求 + +- Xcode 16.0 或更高版本 +- iOS 18 模拟器或真机(最低支持 iOS 14) +- Swift 5.9+ + +### 运行步骤 + +1. 打开 `XuqmGroup-iOSSDK/XuqmDemo/XuqmDemo.xcodeproj`(或 `.xcworkspace`) +2. 等待 Swift Package Manager 依赖解析完成 +3. 在 Xcode 顶部选择目标模拟器(如 iPhone 16 Pro)或连接的真机 +4. 点击 **Run**(⌘+R) +5. 使用演示账号登录验证消息收发 + +### 关键路径 + +``` +Demo 工程:XuqmGroup-iOSSDK/XuqmDemo/XuqmDemo.xcodeproj +SDK 源码:XuqmGroup-iOSSDK/Sources/XuqmSDK/ +单元测试:XuqmGroup-iOSSDK/Tests/ +``` + +### 常见问题 + +- **SPM 依赖下载慢**:检查网络或更换 Xcode → Preferences → Accounts → Git 配置 +- **真机运行失败**:确保 Apple Developer Account 已配置 Signing & Capabilities +- **Push 测试**:模拟器不支持 APNs,Push 功能需在真机测试 diff --git a/docs-site/docs/ios/index.md b/docs-site/docs/ios/index.md index 7cdffaa..364e377 100644 --- a/docs-site/docs/ios/index.md +++ b/docs-site/docs/ios/index.md @@ -6,10 +6,10 @@ | 模块 | 功能 | |------|------| -| XuqmCore | 初始化、网络、鉴权 | -| XuqmIM | 单聊、群聊、消息收发(13 种类型)| -| XuqmPush | APNs 设备 Token 注册、通知处理 | -| XuqmUpdate | App 版本检查、RN Bundle 热更新 | +| XuqmCore | 初始化、网络、鉴权、UserSig 过期检测 | +| XuqmIM | 单聊、群聊、消息收发(15 种类型)、连接状态监听 | +| XuqmPush | APNs 设备 Token 注册、FCM 备选、通知处理 | +| XuqmUpdate | App 版本检查、App Store 跳转 | ## 安装 @@ -35,8 +35,9 @@ dependencies: [ .target( name: "MyApp", dependencies: [ - .product(name: "XuqmCore", package: "XuqmGroup-iOSSDK"), - .product(name: "XuqmIM", package: "XuqmGroup-iOSSDK"), + .product(name: "XuqmCore", package: "XuqmGroup-iOSSDK"), + .product(name: "XuqmIM", package: "XuqmGroup-iOSSDK"), + .product(name: "XuqmPush", package: "XuqmGroup-iOSSDK"), .product(name: "XuqmUpdate", package: "XuqmGroup-iOSSDK"), ] ) @@ -51,91 +52,169 @@ dependencies: [ ```swift import XuqmCore -XuqmSDK.shared.initialize( - appKey: "your_app_key", - appSecret: "your_app_secret", - debug: false -) +let config = SDKConfig(appId: "your_app_id", appSecret: "your_app_secret") +XuqmSDK.shared.initialize(config: config) ``` -### 2. IM 登录与监听消息 +### 2. IM 登录与监听消息(UserSig 模式) ```swift import XuqmIM -// 登录(appKey 已在 init 时指定) -try await ImSDK.shared.login(userId: "user_001", nickname: "张三") +// 方式一:使用 UserSig 登录(推荐生产环境) +try await XuqmSDK.shared.login(userId: "user_001", userSig: "your_user_sig_jwt") -// 监听事件 -ImSDK.shared.addListener(self) +// 方式二:Demo 环境快速登录 +try await ImSDK.shared.loginWithDemo(userId: "user_001", password: "123456") -extension ViewController: ImEventListener { - func onConnected() { print("WS connected") } - func onMessage(_ msg: ImMessage) { /* 处理消息 */ } - func onDisconnected(reason: String?) { /* 断线 */ } +// 设置事件代理 +ImSDK.shared.setDelegate(self) + +extension ViewController: ImEventDelegate { + func imClientDidConnect() { print("WS connected") } + func imClientDidReceiveMessage(_ msg: ImMessage) { /* 处理单聊消息 */ } + func imClientDidReceiveGroupMessage(_ msg: ImMessage) { /* 处理群消息 */ } + func imClientDidReadMessage(_ msg: ImMessage) { /* 对方已读回执 */ } + func imClientDidReceiveRevokedMessage(_ msg: ImMessage) { /* 消息被撤回 */ } + func imClientDidDisconnect(reason: String?) { /* 断线 */ } + func imClientDidError(_ error: String) { /* 错误 */ } } ``` -### 3. 发送消息 +### 3. 连接状态监听 + +SDK 暴露 `connectionState` 属性与 `addConnectionStateListener`,方便业务层实时感知 WebSocket 连接状态: + +```swift +// 查询当前状态 +let state = ImSDK.shared.connectionState +// 返回:.disconnected / .connecting / .connected + +// 注册状态变更监听器 +ImSDK.shared.addConnectionStateListener { state in + switch state { + case .connected: + print("IM 已连接") + case .connecting: + print("IM 连接中...") + case .disconnected: + print("IM 已断开") + } +} +``` + +> 状态变更触发时机: +> - 登录后 → `.connecting` → `.connected` +> - 网络异常/断线 → `.disconnected`,内部自动重连 → `.connecting` → `.connected` +> - 调用 `disconnect()` 或 `logout()` → `.disconnected` + +### 4. 发送消息 ```swift // 发文本 -let sent = try await ImSDK.shared.sendMessage( - toId: "user_002", +let msg = ImSDK.shared.sendTextMessage( + toId: "user_002", chatType: .single, - msgType: .text, - content: "Hello!" + content: "Hello!" ) -// 发图片(content 为 JSON 字符串) +// 发图片 let imgContent = try JSONSerialization.data(withJSONObject: [ "url": "https://cdn.example.com/img.jpg", "width": 800, "height": 600 ]) -let sent = try await ImSDK.shared.sendMessage( +let msg2 = try await ImSDK.shared.sendMessage( toId: "user_002", chatType: .single, msgType: .image, content: String(data: imgContent, encoding: .utf8)! ) ``` -### 4. 撤回消息 +### 5. 撤回与编辑 ```swift +// 撤回消息 let revoked = try await ImSDK.shared.revokeMessage(messageId: msg.id) + +// 编辑消息 +let edited = try await ImSDK.shared.editMessage(messageId: msg.id, content: "新内容") ``` -### 5. 群聊 +### 6. 群聊 ```swift // 创建群组 let group = try await ImSDK.shared.createGroup(name: "我的群", memberIds: ["user_001", "user_002"]) // 订阅群消息 -ImSDK.shared.subscribeGroup(groupId: group.id) +ImSDK.shared.subscribeGroup(group.id) // 发送群消息 -try await ImSDK.shared.sendMessage( - toId: group.id, chatType: .group, msgType: .text, content: "大家好" -) +ImSDK.shared.sendTextMessage(toId: group.id, chatType: .group, content: "大家好") + +// 群管理 + try await ImSDK.shared.addGroupMember(groupId: group.id, userId: "user_003") + try await ImSDK.shared.leaveGroup(groupId: group.id) ``` -### 6. 检查更新 +### 7. 会话管理 + +```swift +// 会话列表 +let conversations = try await ImSDK.shared.listConversations() + +// 置顶 / 静音 / 已读 / 草稿 / 删除 +try await ImSDK.shared.setConversationPinned(targetId: "user_002", chatType: .single, pinned: true) +try await ImSDK.shared.setConversationMuted(targetId: "user_002", chatType: .single, muted: true) +try await ImSDK.shared.markRead(targetId: "user_002", chatType: .single) +try await ImSDK.shared.setDraft(targetId: "user_002", chatType: .single, draft: "未完成") +try await ImSDK.shared.deleteConversation(targetId: "user_002", chatType: .single) +``` + +### 8. Push 设备注册(APNs + FCM) + +```swift +import XuqmPush + +// 请求通知授权并自动注册 APNs +let granted = try await PushSDK.shared.requestAuthorization(options: [.alert, .badge, .sound]) + +// 在 AppDelegate 中转发 deviceToken +func application(_ application: UIApplication, didRegisterForRemoteNotificationsWithDeviceToken deviceToken: Data) { + XuqmSDK.shared.registerDeviceToken(deviceToken) +} + +// 如需手动注册 FCM Token(若集成 FirebaseMessaging) +try await PushSDK.shared.registerFcmToken(fcmToken, userId: "user_001") +``` + +### 9. UserSig 过期检测 + +`XuqmSDK` 会在登录时解析 UserSig JWT 的 `exp` 字段,并在过期前 5 分钟触发 `onUserSigExpired` 回调。业务层应在此回调中重新获取 UserSig 并登录。 + +```swift +// 设置过期回调(建议在初始化后设置) +XuqmSDK.shared.onUserSigExpired = { [weak self] in + Task { + let newUserSig = await self?.yourBackend.refreshUserSig() + try? await XuqmSDK.shared.login(userId: "user_001", userSig: newUserSig) + } +} +``` + +> **注意**: +> - 回调触发时机为 `exp - 300s`(提前 5 分钟)。 +> - 调用 `logout()` 后内部 Timer 会被自动 `invalidate`。 +> - 若 UserSig 已过期(`exp <= now`),回调会立即触发。 + +### 10. 检查更新 ```swift import XuqmUpdate // App 整包更新 let appInfo = try await UpdateSDK.shared.checkAppUpdate(currentVersionCode: 1) -if let info = appInfo, info.forceUpdate { - // 强制更新:跳转 App Store 或下载链接 - UIApplication.shared.open(URL(string: info.downloadUrl ?? info.appStoreUrl!)!) -} - -// RN Bundle 热更新 -let bundle = try await UpdateSDK.shared.checkRNUpdate(moduleId: "home", currentVersion: "1.0.0") -if let bundle = bundle { - let data = try await UpdateSDK.shared.downloadBundle(url: bundle.downloadUrl) - // 缓存至本地,下次启动时由 BundleRuntime 加载 +if let info = appInfo, info.forceUpdate == true { + UpdateSDK.shared.openAppStore(url: info.appStoreUrl ?? info.downloadUrl!) } ``` @@ -150,8 +229,11 @@ if let bundle = bundle { | `.file` | 文件 | `{url, name, size, mimeType}` | | `.location` | 位置 | `{lat, lng, address, title}` | | `.custom` | 自定义 | 任意 JSON | -| `.notify` | 系统通知 | `{title, content, level}` | +| `.notify` | 系统通知 | `{title, content}` | | `.richText` | 富文本 | `{html}` | | `.callAudio` | 语音通话信令 | `{callId, action, callerName}` | | `.callVideo` | 视频通话信令 | `{callId, action, callerName}` | | `.forward` | 转发 | `{originalMsgId, originalContent, originalSender}` | +| `.quote` | 引用 | `{quotedMsgId, quotedContent, text}` | +| `.merge` | 合并转发 | `{title, msgList}` | +| `.revoked` | 撤回 | 系统内部填充 | diff --git a/ops-platform/components.d.ts b/ops-platform/components.d.ts index 3d053cc..1daf8d2 100644 --- a/ops-platform/components.d.ts +++ b/ops-platform/components.d.ts @@ -12,6 +12,8 @@ declare module 'vue' { ElCard: typeof import('element-plus/es')['ElCard'] ElCol: typeof import('element-plus/es')['ElCol'] ElContainer: typeof import('element-plus/es')['ElContainer'] + ElDescriptions: typeof import('element-plus/es')['ElDescriptions'] + ElDescriptionsItem: typeof import('element-plus/es')['ElDescriptionsItem'] ElDialog: typeof import('element-plus/es')['ElDialog'] ElDrawer: typeof import('element-plus/es')['ElDrawer'] ElForm: typeof import('element-plus/es')['ElForm'] @@ -19,14 +21,17 @@ declare module 'vue' { ElHeader: typeof import('element-plus/es')['ElHeader'] ElIcon: typeof import('element-plus/es')['ElIcon'] ElInput: typeof import('element-plus/es')['ElInput'] + ElInputNumber: typeof import('element-plus/es')['ElInputNumber'] ElMain: typeof import('element-plus/es')['ElMain'] ElMenu: typeof import('element-plus/es')['ElMenu'] ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] ElOption: typeof import('element-plus/es')['ElOption'] + ElPageHeader: typeof import('element-plus/es')['ElPageHeader'] ElPagination: typeof import('element-plus/es')['ElPagination'] ElRow: typeof import('element-plus/es')['ElRow'] ElSelect: typeof import('element-plus/es')['ElSelect'] ElStatistic: typeof import('element-plus/es')['ElStatistic'] + ElSwitch: typeof import('element-plus/es')['ElSwitch'] ElTable: typeof import('element-plus/es')['ElTable'] ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] ElTag: typeof import('element-plus/es')['ElTag'] diff --git a/ops-platform/package.json b/ops-platform/package.json index d7f940c..f1844db 100644 --- a/ops-platform/package.json +++ b/ops-platform/package.json @@ -9,9 +9,10 @@ "preview": "vite preview" }, "dependencies": { - "axios": "^1.7.9", - "element-plus": "^2.9.1", "@element-plus/icons-vue": "^2.3.1", + "axios": "^1.7.9", + "echarts": "^6.0.0", + "element-plus": "^2.9.1", "pinia": "^3.0.1", "vue": "^3.5.13", "vue-router": "^4.5.0" @@ -19,9 +20,9 @@ "devDependencies": { "@vitejs/plugin-vue": "^5.2.3", "typescript": "^5.8.2", - "vite": "^6.2.2", - "vue-tsc": "^2.2.8", "unplugin-auto-import": "^0.18.2", - "unplugin-vue-components": "^0.27.4" + "unplugin-vue-components": "^0.27.4", + "vite": "^6.2.2", + "vue-tsc": "^2.2.8" } } diff --git a/ops-platform/src/api/ops.ts b/ops-platform/src/api/ops.ts index 7eba949..5bbb5e1 100644 --- a/ops-platform/src/api/ops.ts +++ b/ops-platform/src/api/ops.ts @@ -12,6 +12,13 @@ export interface TenantItem { createdAt: string } +export interface TenantDetail extends TenantItem { + appCount: number + subAccountCount: number + activeServiceCount: number + apps: AppItem[] +} + export interface TenantPage { content: TenantItem[] total: number @@ -43,6 +50,80 @@ export interface ServiceRequestPage { totalPages: number } +export interface AppItem { + id: string + appKey: string + appSecret: string + name: string + packageName: string + tenantId: string + createdAt: string +} + +export interface FeatureServiceItem { + id: string + appId: string + platform: string + serviceType: string + enabled: boolean + config?: string | null + createdAt: string +} + +export interface AppDetail { + app: AppItem + tenant: TenantItem | null + services: FeatureServiceItem[] + serviceCount: number + enabledServiceCount: number +} + +export interface AppPage { + content: AppItem[] + total: number + totalPages: number +} + +export interface OpsLogItem { + id: string + tenantId: string + moduleType: string + resourceType: string + resourceId: string + action: string + operator: string + detailJson: string + createdAt: string +} + +export interface OpsLogPage { + content: OpsLogItem[] + total: number + totalPages: number +} + +export interface RiskRuleForm { + ipRateLimit: number + loginFailThreshold: number + loginLockMinutes: number + abnormalDetection: boolean +} + +export interface SensitiveWord { + id: string + word: string + level: '高' | '中' | '低' + category: string + enabled: boolean + updatedAt: string +} + +export interface SensitiveWordPage { + content: SensitiveWord[] + total: number + totalPages: number +} + export const opsApi = { listTenants: (keyword = '', page = 0, size = 20) => client.get<{ data: TenantPage }>('/ops/tenants', { params: { keyword, page, size } }), @@ -50,6 +131,12 @@ export const opsApi = { toggleStatus: (id: string) => client.post(`/ops/tenants/${id}/toggle-status`), + getTenant: (id: string) => + client.get<{ data: TenantDetail }>(`/ops/tenants/${id}`), + + listTenantApps: (id: string) => + client.get<{ data: AppItem[] }>(`/ops/tenants/${id}/apps`), + statistics: () => client.get<{ data: Statistics }>('/ops/statistics'), @@ -61,4 +148,38 @@ export const opsApi = { rejectRequest: (requestId: string, reviewNote = '') => client.post<{ data: ServiceRequest }>(`/ops/service-requests/${requestId}/reject`, { reviewNote }), + + listApps: (keyword = '', page = 0, size = 20) => + client.get<{ data: AppPage }>('/ops/apps', { params: { keyword, page, size } }), + + getApp: (id: string) => + client.get<{ data: AppDetail }>(`/ops/apps/${id}`), + + listAppServices: (id: string) => + client.get<{ data: FeatureServiceItem[] }>(`/ops/apps/${id}/services`), + + listOperationLogs: (page = 0, size = 20) => + client.get<{ data: OpsLogPage }>('/ops/operation-logs', { params: { page, size } }), + + // 风控相关 API(mock 实现,TODO:接入后端) + getRiskRules: () => + client.get<{ data: RiskRuleForm }>('/ops/risk/rules'), + + saveRiskRules: (rules: RiskRuleForm) => + client.post('/ops/risk/rules', rules), + + listSensitiveWords: (page = 0, size = 20) => + client.get<{ data: SensitiveWordPage }>('/ops/risk/sensitive-words', { params: { page, size } }), + + createSensitiveWord: (word: Omit) => + client.post<{ data: SensitiveWord }>('/ops/risk/sensitive-words', word), + + updateSensitiveWord: (id: string, word: Partial) => + client.put<{ data: SensitiveWord }>(`/ops/risk/sensitive-words/${id}`, word), + + toggleSensitiveWord: (id: string, enabled: boolean) => + client.patch(`/ops/risk/sensitive-words/${id}/toggle`, { enabled }), + + deleteSensitiveWord: (id: string) => + client.delete(`/ops/risk/sensitive-words/${id}`), } diff --git a/ops-platform/src/router/index.ts b/ops-platform/src/router/index.ts index 9183edf..719b83e 100644 --- a/ops-platform/src/router/index.ts +++ b/ops-platform/src/router/index.ts @@ -11,8 +11,13 @@ const router = createRouter({ children: [ { path: '', redirect: '/tenants' }, { path: 'tenants', component: () => import('@/views/tenants/TenantListView.vue') }, + { path: 'tenants/:id', component: () => import('@/views/tenants/TenantDetailView.vue') }, { path: 'statistics', component: () => import('@/views/statistics/StatisticsView.vue') }, { path: 'service-requests', component: () => import('@/views/services/ServiceRequestsView.vue') }, + { path: 'apps', component: () => import('@/views/apps/AppListView.vue') }, + { path: 'apps/:id', component: () => import('@/views/apps/AppDetailView.vue') }, + { path: 'operation-logs', component: () => import('@/views/logs/OperationLogView.vue') }, + { path: 'risk-control', component: () => import('@/views/risk/RiskControlView.vue') }, ], }, ], diff --git a/ops-platform/src/views/apps/AppDetailView.vue b/ops-platform/src/views/apps/AppDetailView.vue new file mode 100644 index 0000000..2d3f8e8 --- /dev/null +++ b/ops-platform/src/views/apps/AppDetailView.vue @@ -0,0 +1,87 @@ + + + + + diff --git a/ops-platform/src/views/apps/AppListView.vue b/ops-platform/src/views/apps/AppListView.vue new file mode 100644 index 0000000..9201aa5 --- /dev/null +++ b/ops-platform/src/views/apps/AppListView.vue @@ -0,0 +1,59 @@ + + + diff --git a/ops-platform/src/views/layout/MainLayout.vue b/ops-platform/src/views/layout/MainLayout.vue index 63386f1..bdc691e 100644 --- a/ops-platform/src/views/layout/MainLayout.vue +++ b/ops-platform/src/views/layout/MainLayout.vue @@ -66,7 +66,7 @@ + + diff --git a/ops-platform/src/views/risk/RiskControlView.vue b/ops-platform/src/views/risk/RiskControlView.vue new file mode 100644 index 0000000..56835d9 --- /dev/null +++ b/ops-platform/src/views/risk/RiskControlView.vue @@ -0,0 +1,283 @@ + + + + + diff --git a/ops-platform/src/views/statistics/StatisticsView.vue b/ops-platform/src/views/statistics/StatisticsView.vue index 4f94de2..b97b50e 100644 --- a/ops-platform/src/views/statistics/StatisticsView.vue +++ b/ops-platform/src/views/statistics/StatisticsView.vue @@ -1,26 +1,47 @@ + + diff --git a/ops-platform/src/views/tenants/TenantDetailView.vue b/ops-platform/src/views/tenants/TenantDetailView.vue new file mode 100644 index 0000000..3b2997f --- /dev/null +++ b/ops-platform/src/views/tenants/TenantDetailView.vue @@ -0,0 +1,104 @@ + + + + + diff --git a/ops-platform/src/views/tenants/TenantListView.vue b/ops-platform/src/views/tenants/TenantListView.vue index 3f63a33..3304270 100644 --- a/ops-platform/src/views/tenants/TenantListView.vue +++ b/ops-platform/src/views/tenants/TenantListView.vue @@ -30,6 +30,9 @@