docs(deploy): 添加完整的部署文档和配置示例
- 新增 compose.production.yaml 和 compose.production.server.yaml 部署配置 - 添加 nginx.dev.xuqinmin.com.conf 和 nginx.sentry.xuqinmin.com.conf 反向代理配置 - 创建详细的部署指南文档 deploy/README.md,涵盖架构设计和部署步骤 - 添加前端访问文档 web/README.md,包含线上地址和接口说明 - 补充平台文档总览 README.md,整合各模块文档入口 - 配置多服务容器化部署,包括 tenant-service、im-service、push-service 等 - 设置外部数据库和 Redis 连接配置,确保服务间正确通信 - 配置 WebSocket 和 API 路由转发规则,支持实时通信和版本更新服务
这个提交包含在:
父节点
3151df4054
当前提交
09891bf46e
@ -109,6 +109,8 @@ JWT Payload 由 `atob(token.split('.')[1])` 解析,无需额外请求。
|
|||||||
## 运营管理平台(ops-platform)`:5174`
|
## 运营管理平台(ops-platform)`:5174`
|
||||||
|
|
||||||
> 内部使用,管理所有租户、查看统计数据。独立账号体系。
|
> 内部使用,管理所有租户、查看统计数据。独立账号体系。
|
||||||
|
>
|
||||||
|
> 生产环境独立域名:`https://ops.xuqinmin.com/`
|
||||||
|
|
||||||
### 路由结构
|
### 路由结构
|
||||||
|
|
||||||
@ -153,10 +155,10 @@ API 请求通过 `src/api/client.ts` 中的 axios 实例统一附加,401 跳
|
|||||||
|
|
||||||
```env
|
```env
|
||||||
# tenant-platform
|
# tenant-platform
|
||||||
VITE_API_BASE_URL=https://api.xuqm.com/api
|
VITE_API_BASE_URL=https://dev.xuqinmin.com/api
|
||||||
|
|
||||||
# ops-platform
|
# ops-platform
|
||||||
VITE_API_BASE_URL=https://api.xuqm.com/api
|
VITE_API_BASE_URL=https://ops.xuqinmin.com/api
|
||||||
```
|
```
|
||||||
|
|
||||||
未设置时默认代理到 `http://localhost:8081`(Vite dev server proxy)。
|
未设置时默认代理到 `http://localhost:8081`(Vite dev server proxy)。
|
||||||
|
|||||||
@ -21,9 +21,12 @@ UserSig 是 XuqmGroup 的登录鉴权凭证,由业务服务端用 `appSecret`
|
|||||||
|
|
||||||
### 特点
|
### 特点
|
||||||
|
|
||||||
- 当前版本不过期,只校验 `userId + UserSig` 是否匹配
|
- 客户端 IM 登录使用 `userId + UserSig`
|
||||||
|
- 服务端 SDK 可本地生成和校验 UserSig
|
||||||
|
- 当前版本支持过期时间,签发时可按业务控制有效期
|
||||||
- `appSecret` **绝不下发到客户端**
|
- `appSecret` **绝不下发到客户端**
|
||||||
- 若需撤销权限,可在租户平台重置 `appSecret` 或拉黑账号
|
- 若需撤销权限,可在租户平台重置 `appSecret` 或拉黑账号
|
||||||
|
- 注册用户可标记为管理员;管理员账号可用于服务端 SDK / 管理端 REST API
|
||||||
|
|
||||||
### 签发方式(示例)
|
### 签发方式(示例)
|
||||||
|
|
||||||
@ -32,9 +35,9 @@ UserSig 是 XuqmGroup 的登录鉴权凭证,由业务服务端用 `appSecret`
|
|||||||
import jwt from 'jsonwebtoken'
|
import jwt from 'jsonwebtoken'
|
||||||
|
|
||||||
const userSig = jwt.sign(
|
const userSig = jwt.sign(
|
||||||
{ userId: 'user_001', appKey: 'your_app_key' },
|
{ userId: 'user_001', appKey: 'your_app_key', iat: Math.floor(Date.now() / 1000) },
|
||||||
'your_app_secret',
|
'your_app_secret',
|
||||||
{ algorithm: 'HS256' }
|
{ algorithm: 'HS256', expiresIn: '180d' }
|
||||||
)
|
)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|||||||
@ -43,6 +43,8 @@ XuqmSDK.shared.initialize(config: config)
|
|||||||
|
|
||||||
## 4. 服务端签发 UserSig
|
## 4. 服务端签发 UserSig
|
||||||
|
|
||||||
|
服务端可以通过 SDK 本地生成 `UserSig`,也可以通过 IM 管理页生成并校验。
|
||||||
|
如果账号需要用于服务端 SDK / 管理端 REST API,请把该注册用户标记为管理员。
|
||||||
|
|
||||||
### 签发逻辑(示例)
|
### 签发逻辑(示例)
|
||||||
|
|
||||||
@ -51,8 +53,9 @@ XuqmSDK.shared.initialize(config: config)
|
|||||||
import jwt from 'jsonwebtoken'
|
import jwt from 'jsonwebtoken'
|
||||||
|
|
||||||
return jwt.sign(
|
return jwt.sign(
|
||||||
{ userId, appKey },
|
{ userId, appKey, iat: Math.floor(Date.now() / 1000) },
|
||||||
{ algorithm: 'HS256' }
|
appSecret,
|
||||||
|
{ algorithm: 'HS256', expiresIn: '180d' }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
@ -60,10 +63,11 @@ import jwt from 'jsonwebtoken'
|
|||||||
```python
|
```python
|
||||||
# Python
|
# Python
|
||||||
import jwt
|
import jwt
|
||||||
|
import time
|
||||||
|
|
||||||
def generate_user_sig(user_id: str, app_key: str, app_secret: str) -> str:
|
def generate_user_sig(user_id: str, app_key: str, app_secret: str) -> str:
|
||||||
return jwt.encode(
|
return jwt.encode(
|
||||||
{"userId": user_id, "appKey": app_key},
|
{"userId": user_id, "appKey": app_key, "iat": int(time.time())},
|
||||||
app_secret,
|
app_secret,
|
||||||
algorithm="HS256"
|
algorithm="HS256"
|
||||||
)
|
)
|
||||||
@ -72,10 +76,12 @@ def generate_user_sig(user_id: str, app_key: str, app_secret: str) -> str:
|
|||||||
```go
|
```go
|
||||||
// Go
|
// Go
|
||||||
import "github.com/golang-jwt/jwt/v5"
|
import "github.com/golang-jwt/jwt/v5"
|
||||||
|
import "time"
|
||||||
|
|
||||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
token := jwt.NewWithClaims(jwt.SigningMethodHS256, jwt.MapClaims{
|
||||||
"userId": userID,
|
"userId": userID,
|
||||||
"appKey": appKey,
|
"appKey": appKey,
|
||||||
|
"iat": time.Now().Unix(),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|||||||
@ -16,9 +16,14 @@ XuqmGroup 的服务端能力分成三类:
|
|||||||
|
|
||||||
### IM 服务
|
### IM 服务
|
||||||
|
|
||||||
- `POST /api/im/auth/login` 之前的所有请求,客户端和服务端均需携带 `Authorization: Bearer <im_jwt>`
|
- `POST /api/im/auth/login` 返回 `im_jwt`
|
||||||
- `im_jwt` 通过 `POST /api/im/auth/login` 颁发
|
- 客户端登录使用 `userId + userSig`
|
||||||
- 登录请求需要 `X-App-Timestamp`、`X-App-Nonce`、`X-App-Signature`
|
- `userSig` 由服务端或 IM 管理页生成,服务端 SDK 可本地生成和校验
|
||||||
|
- `GET /api/im/platform-events/token` 由 IM 服务返回平台事件账号 `platform` 的登录 token,用于租户平台实时刷新
|
||||||
|
- 服务端管理类调用使用具备管理员权限的 IM 账号登录后获得的 `im_jwt`
|
||||||
|
- 服务端如果不想先走 SDK,也可以直接调用 `/api/im/admin/users/{userId}/usersig` 生成 `userSig`,再用 `/api/im/auth/login?userSig=...` 换取 `im_jwt`
|
||||||
|
- 只有被标记为管理员的 IM 注册账号,才允许用于服务端 SDK / 管理端 REST API
|
||||||
|
- IM 管理页里的 `UserSig` 生成 / 校验工具也只对管理员账号开放
|
||||||
|
|
||||||
### Push / Update / Tenant 相关服务
|
### Push / Update / Tenant 相关服务
|
||||||
|
|
||||||
@ -34,7 +39,9 @@ XuqmGroup 的服务端能力分成三类:
|
|||||||
|
|
||||||
| 方法 | 路径 | 说明 |
|
| 方法 | 路径 | 说明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| `POST` | `/api/im/auth/login` | IM 登录,返回 JWT Token |
|
| `POST` | `/api/im/auth/login` | IM 登录,返回 JWT Token;支持 `userSig` |
|
||||||
|
| `POST` | `/api/im/admin/users/{userId}/usersig` | 生成 UserSig |
|
||||||
|
| `POST` | `/api/im/admin/users/{userId}/usersig/verify` | 校验 UserSig |
|
||||||
| `GET` | `/api/im/accounts/{userId}` | 获取账号资料 |
|
| `GET` | `/api/im/accounts/{userId}` | 获取账号资料 |
|
||||||
| `PUT` | `/api/im/accounts/{userId}` | 更新账号资料 |
|
| `PUT` | `/api/im/accounts/{userId}` | 更新账号资料 |
|
||||||
| `GET` | `/api/im/accounts/search` | 搜索账号 |
|
| `GET` | `/api/im/accounts/search` | 搜索账号 |
|
||||||
@ -177,6 +184,9 @@ XuqmGroup 的服务端能力分成三类:
|
|||||||
| `GET` | `/api/push/admin/device-logs` | 设备日志 |
|
| `GET` | `/api/push/admin/device-logs` | 设备日志 |
|
||||||
| `POST` | `/api/push/admin/test-offline` | 离线推送测试 |
|
| `POST` | `/api/push/admin/test-offline` | 离线推送测试 |
|
||||||
|
|
||||||
|
> Push 服务不使用 UserSig;公开接口面向业务服务,管理端接口继续使用平台登录态或服务端管理态。
|
||||||
|
> Push SDK 与 REST API 的公开调用面向业务系统,管理员诊断能力通过平台 JWT 或服务端管理态访问。
|
||||||
|
|
||||||
### 内部接口
|
### 内部接口
|
||||||
|
|
||||||
| 方法 | 路径 | 说明 |
|
| 方法 | 路径 | 说明 |
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
XuqmGroup 服务端 Go SDK,按照腾讯云服务端 API 的分类方式封装了:
|
XuqmGroup 服务端 Go SDK,按照腾讯云服务端 API 的分类方式封装了:
|
||||||
|
|
||||||
- IM 账号、消息、群组、好友、会话、黑名单
|
- IM 账号、UserSig 生成与登录、消息、群组、好友、会话、黑名单
|
||||||
- 管理端 Webhook、统计、操作日志
|
- 管理端 Webhook、统计、操作日志
|
||||||
- Push 注册与推送
|
- Push 注册与推送
|
||||||
- Update 版本管理、RN Bundle、应用商店提审
|
- Update 版本管理、RN Bundle、应用商店提审
|
||||||
@ -36,11 +36,23 @@ if err != nil {
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## UserSig
|
||||||
|
|
||||||
|
```go
|
||||||
|
userSig := client.GenerateUserSig("user_001")
|
||||||
|
valid := client.VerifyUserSig("user_001", userSig)
|
||||||
|
login, err := client.LoginWithUserSig("user_001", userSig)
|
||||||
|
```
|
||||||
|
|
||||||
|
管理员账号可用于服务端 SDK / 管理端 REST API。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 能力分类
|
## 能力分类
|
||||||
|
|
||||||
| 分类 | 主要方法 |
|
| 分类 | 主要方法 |
|
||||||
|------|----------|
|
|------|----------|
|
||||||
| 登录 | `Login` |
|
| 登录 | `GenerateUserSig`, `VerifyUserSig`, `Login`, `LoginWithUserSig` |
|
||||||
| 账号 | `ImportAccount`, `ImportAccounts`, `DeleteAccount`, `GetProfile`, `UpdateProfile`, `SearchAccounts`, `CheckAccount` |
|
| 账号 | `ImportAccount`, `ImportAccounts`, `DeleteAccount`, `GetProfile`, `UpdateProfile`, `SearchAccounts`, `CheckAccount` |
|
||||||
| 消息 | `SendMessage`, `RevokeMessage`, `EditMessage`, `FetchHistory`, `FetchGroupHistory`, `SearchMessages` |
|
| 消息 | `SendMessage`, `RevokeMessage`, `EditMessage`, `FetchHistory`, `FetchGroupHistory`, `SearchMessages` |
|
||||||
| 会话 | `ListConversations`, `SetConversationPinned`, `SetConversationMuted`, `MarkRead`, `SetDraft`, `SetConversationHidden`, `SetConversationGroup`, `DeleteConversation` |
|
| 会话 | `ListConversations`, `SetConversationPinned`, `SetConversationMuted`, `MarkRead`, `SetDraft`, `SetConversationHidden`, `SetConversationGroup`, `DeleteConversation` |
|
||||||
@ -49,7 +61,7 @@ if err != nil {
|
|||||||
| 群组 | `ListGroups`, `ListPublicGroups`, `SearchGroups`, `GetGroup`, `ListGroupMembers`, `SearchGroupMembers`, `CreateGroup`, `UpdateGroup`, `AddGroupMember`, `AddGroupMembers`, `RemoveGroupMember`, `RemoveGroupMembers`, `SetGroupRole`, `TransferGroupOwner`, `LeaveGroup`, `UpdateGroupAttributes`, `RemoveGroupAttributes`, `MuteGroupMember`, `DismissGroup`, `SendGroupJoinRequest`, `ListGroupJoinRequests`, `AcceptGroupJoinRequest`, `RejectGroupJoinRequest`, `AcceptGroupJoinRequests`, `RejectGroupJoinRequests` |
|
| 群组 | `ListGroups`, `ListPublicGroups`, `SearchGroups`, `GetGroup`, `ListGroupMembers`, `SearchGroupMembers`, `CreateGroup`, `UpdateGroup`, `AddGroupMember`, `AddGroupMembers`, `RemoveGroupMember`, `RemoveGroupMembers`, `SetGroupRole`, `TransferGroupOwner`, `LeaveGroup`, `UpdateGroupAttributes`, `RemoveGroupAttributes`, `MuteGroupMember`, `DismissGroup`, `SendGroupJoinRequest`, `ListGroupJoinRequests`, `AcceptGroupJoinRequest`, `RejectGroupJoinRequest`, `AcceptGroupJoinRequests`, `RejectGroupJoinRequests` |
|
||||||
| 管理端 | `QueryUserState`, `KickUsers`, `BatchSendMessage`, `AdminSetMsgRead`, `ImportMessages`, `AdminTransferGroupOwner`, `AdminUpdateGroupAttributes`, `AdminRemoveGroupAttributes`, `AdminGroupReadReceipts` |
|
| 管理端 | `QueryUserState`, `KickUsers`, `BatchSendMessage`, `AdminSetMsgRead`, `ImportMessages`, `AdminTransferGroupOwner`, `AdminUpdateGroupAttributes`, `AdminRemoveGroupAttributes`, `AdminGroupReadReceipts` |
|
||||||
| Webhook | `ListWebhooks`, `CreateWebhook`, `UpdateWebhook`, `DeleteWebhook`, `VerifyCallbackSignature`, `ParseCallbackEnvelope` |
|
| Webhook | `ListWebhooks`, `CreateWebhook`, `UpdateWebhook`, `DeleteWebhook`, `VerifyCallbackSignature`, `ParseCallbackEnvelope` |
|
||||||
| Push | `RegisterPushToken`, `SendPush` |
|
| Push | `RegisterPushToken`, `SendPush`, `PushUserStatus`, `PushDeviceLogs`, `TestOfflinePush` |
|
||||||
| Update | `CheckAppUpdate`, `UploadAppVersion`, `PublishAppVersion`, `UnpublishAppVersion`, `GrayAppVersion`, `ListAppVersions`, `CheckRnUpdate`, `UploadRnBundle`, `PublishRnBundle`, `UnpublishRnBundle`, `ListRnBundles` |
|
| Update | `CheckAppUpdate`, `UploadAppVersion`, `PublishAppVersion`, `UnpublishAppVersion`, `GrayAppVersion`, `ListAppVersions`, `CheckRnUpdate`, `UploadRnBundle`, `PublishRnBundle`, `UnpublishRnBundle`, `ListRnBundles` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
XuqmGroup 服务端 Java SDK,按腾讯云服务端 API 的思路封装了以下能力:
|
XuqmGroup 服务端 Java SDK,按腾讯云服务端 API 的思路封装了以下能力:
|
||||||
|
|
||||||
- IM 账号与登录
|
- IM 账号、UserSig 生成与登录
|
||||||
- 消息发送、编辑、撤回、历史查询
|
- 消息发送、编辑、撤回、历史查询
|
||||||
- 好友、黑名单、会话、群组
|
- 好友、黑名单、会话、群组
|
||||||
- Webhook、管理端操作、统计
|
- Webhook、管理端操作、统计
|
||||||
@ -58,6 +58,18 @@ XuqmImServerSdk client = XuqmImServerSdk.builder()
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## UserSig
|
||||||
|
|
||||||
|
```java
|
||||||
|
String userSig = client.generateUserSig("user_001");
|
||||||
|
boolean valid = client.verifyUserSig("user_001", userSig);
|
||||||
|
var login = client.loginWithUserSig("user_001", userSig);
|
||||||
|
```
|
||||||
|
|
||||||
|
管理员账号可用于服务端 SDK / 管理端 REST API;建议在 IM 管理页先给对应注册用户打上管理员标记。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 发送消息
|
## 发送消息
|
||||||
|
|
||||||
```java
|
```java
|
||||||
@ -100,7 +112,7 @@ client.revokeMessage("message_id");
|
|||||||
|
|
||||||
| 分类 | 主要方法 |
|
| 分类 | 主要方法 |
|
||||||
|------|----------|
|
|------|----------|
|
||||||
| IM 账号 | `login`, `importAccount`, `importAccounts`, `deleteAccount`, `getProfile`, `updateProfile`, `searchAccounts` |
|
| IM 账号 | `generateUserSig`, `verifyUserSig`, `login`, `loginWithUserSig`, `importAccount`, `importAccounts`, `deleteAccount`, `getProfile`, `updateProfile`, `searchAccounts` |
|
||||||
| 消息 | `sendMessage`, `revokeMessage`, `editMessage`, `fetchHistory`, `fetchGroupHistory`, `searchMessages` |
|
| 消息 | `sendMessage`, `revokeMessage`, `editMessage`, `fetchHistory`, `fetchGroupHistory`, `searchMessages` |
|
||||||
| 会话 | `listConversations`, `setConversationPinned`, `setConversationMuted`, `markRead`, `setDraft`, `setConversationHidden`, `setConversationGroup`, `deleteConversation` |
|
| 会话 | `listConversations`, `setConversationPinned`, `setConversationMuted`, `markRead`, `setDraft`, `setConversationHidden`, `setConversationGroup`, `deleteConversation` |
|
||||||
| 好友 | `listFriends`, `addFriend`, `addFriends`, `removeFriend`, `removeFriends`, `removeAllFriends`, `setFriendGroup`, `listFriendGroups`, `listFriendsByGroup`, `checkFriends` |
|
| 好友 | `listFriends`, `addFriend`, `addFriends`, `removeFriend`, `removeFriends`, `removeAllFriends`, `setFriendGroup`, `listFriendGroups`, `listFriendsByGroup`, `checkFriends` |
|
||||||
@ -108,7 +120,7 @@ client.revokeMessage("message_id");
|
|||||||
| 群组 | `listGroups`, `createGroup`, `updateGroup`, `addGroupMember`, `addGroupMembers`, `removeGroupMember`, `removeGroupMembers`, `setGroupRole`, `transferGroupOwner`, `leaveGroup`, `updateGroupAttributes`, `removeGroupAttributes`, `muteGroupMember`, `dismissGroup`, `sendGroupJoinRequest`, `listGroupJoinRequests`, `acceptGroupJoinRequest`, `rejectGroupJoinRequest`, `acceptGroupJoinRequests`, `rejectGroupJoinRequests` |
|
| 群组 | `listGroups`, `createGroup`, `updateGroup`, `addGroupMember`, `addGroupMembers`, `removeGroupMember`, `removeGroupMembers`, `setGroupRole`, `transferGroupOwner`, `leaveGroup`, `updateGroupAttributes`, `removeGroupAttributes`, `muteGroupMember`, `dismissGroup`, `sendGroupJoinRequest`, `listGroupJoinRequests`, `acceptGroupJoinRequest`, `rejectGroupJoinRequest`, `acceptGroupJoinRequests`, `rejectGroupJoinRequests` |
|
||||||
| 管理端 | `queryUserState`, `kickUsers`, `batchSendMessage`, `adminSetMsgRead`, `importMessages`, `adminTransferGroupOwner`, `adminUpdateGroupAttributes`, `adminRemoveGroupAttributes`, `adminGroupReadReceipts` |
|
| 管理端 | `queryUserState`, `kickUsers`, `batchSendMessage`, `adminSetMsgRead`, `importMessages`, `adminTransferGroupOwner`, `adminUpdateGroupAttributes`, `adminRemoveGroupAttributes`, `adminGroupReadReceipts` |
|
||||||
| Webhook | `listWebhooks`, `createWebhook`, `updateWebhook`, `deleteWebhook`, `verifyCallbackSignature`, `parseCallbackEnvelope`, 各类 `parse*CallbackPayload` |
|
| Webhook | `listWebhooks`, `createWebhook`, `updateWebhook`, `deleteWebhook`, `verifyCallbackSignature`, `parseCallbackEnvelope`, 各类 `parse*CallbackPayload` |
|
||||||
| Push | `registerPushToken`, `sendPush` |
|
| Push | `registerPushToken`, `sendPush`, `pushUserStatus`, `pushDeviceLogs`, `testOfflinePush` |
|
||||||
| Update | `checkAppUpdate`, `uploadAppVersion`, `publishAppVersion`, `unpublishAppVersion`, `grayAppVersion`, `listAppVersions`, `checkRnUpdate`, `uploadRnBundle`, `publishRnBundle`, `unpublishRnBundle`, `listRnBundles` |
|
| Update | `checkAppUpdate`, `uploadAppVersion`, `publishAppVersion`, `unpublishAppVersion`, `grayAppVersion`, `listAppVersions`, `checkRnUpdate`, `uploadRnBundle`, `publishRnBundle`, `unpublishRnBundle`, `listRnBundles` |
|
||||||
|
|
||||||
## 群管理示例
|
## 群管理示例
|
||||||
|
|||||||
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
XuqmGroup 服务端 Python SDK,按照腾讯云服务端 API 的分类方式封装了:
|
XuqmGroup 服务端 Python SDK,按照腾讯云服务端 API 的分类方式封装了:
|
||||||
|
|
||||||
- IM 账号、消息、群组、好友、会话、黑名单
|
- IM 账号、UserSig 生成与登录、消息、群组、好友、会话、黑名单
|
||||||
- 管理端 Webhook、统计、操作日志
|
- 管理端 Webhook、统计、操作日志
|
||||||
- Push 注册与推送
|
- Push 注册与推送
|
||||||
- Update 版本管理、RN Bundle、应用商店提审
|
- Update 版本管理、RN Bundle、应用商店提审
|
||||||
@ -33,11 +33,23 @@ sdk = XuqmImServerSdk(
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## UserSig
|
||||||
|
|
||||||
|
```python
|
||||||
|
user_sig = sdk.generate_user_sig("user_001")
|
||||||
|
valid = sdk.verify_user_sig("user_001", user_sig)
|
||||||
|
login = sdk.login_with_user_sig("user_001", user_sig)
|
||||||
|
```
|
||||||
|
|
||||||
|
管理员账号可用于服务端 SDK / 管理端 REST API。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## 能力分类
|
## 能力分类
|
||||||
|
|
||||||
| 分类 | 主要方法 |
|
| 分类 | 主要方法 |
|
||||||
|------|----------|
|
|------|----------|
|
||||||
| 登录 | `login` |
|
| 登录 | `generate_user_sig`, `verify_user_sig`, `login`, `login_with_user_sig` |
|
||||||
| 账号 | `import_account`, `import_accounts`, `delete_account`, `get_profile`, `update_profile`, `search_accounts`, `check_account` |
|
| 账号 | `import_account`, `import_accounts`, `delete_account`, `get_profile`, `update_profile`, `search_accounts`, `check_account` |
|
||||||
| 消息 | `send_message`, `revoke_message`, `edit_message`, `fetch_history`, `fetch_group_history`, `search_messages` |
|
| 消息 | `send_message`, `revoke_message`, `edit_message`, `fetch_history`, `fetch_group_history`, `search_messages` |
|
||||||
| 会话 | `list_conversations`, `set_conversation_pinned`, `set_conversation_muted`, `mark_read`, `set_draft`, `set_conversation_hidden`, `set_conversation_group`, `delete_conversation` |
|
| 会话 | `list_conversations`, `set_conversation_pinned`, `set_conversation_muted`, `mark_read`, `set_draft`, `set_conversation_hidden`, `set_conversation_group`, `delete_conversation` |
|
||||||
@ -46,7 +58,7 @@ sdk = XuqmImServerSdk(
|
|||||||
| 群组 | `list_groups`, `list_public_groups`, `search_groups`, `get_group`, `list_group_members`, `search_group_members`, `create_group`, `update_group`, `add_group_member`, `add_group_members`, `remove_group_member`, `remove_group_members`, `set_group_role`, `transfer_group_owner`, `leave_group`, `update_group_attributes`, `remove_group_attributes`, `mute_group_member`, `dismiss_group`, `send_group_join_request`, `list_group_join_requests`, `accept_group_join_request`, `reject_group_join_request`, `accept_group_join_requests`, `reject_group_join_requests` |
|
| 群组 | `list_groups`, `list_public_groups`, `search_groups`, `get_group`, `list_group_members`, `search_group_members`, `create_group`, `update_group`, `add_group_member`, `add_group_members`, `remove_group_member`, `remove_group_members`, `set_group_role`, `transfer_group_owner`, `leave_group`, `update_group_attributes`, `remove_group_attributes`, `mute_group_member`, `dismiss_group`, `send_group_join_request`, `list_group_join_requests`, `accept_group_join_request`, `reject_group_join_request`, `accept_group_join_requests`, `reject_group_join_requests` |
|
||||||
| 管理端 | `query_user_state`, `kick_users`, `batch_send_message`, `admin_set_msg_read`, `import_messages`, `admin_transfer_group_owner`, `admin_update_group_attributes`, `admin_remove_group_attributes`, `admin_group_read_receipts` |
|
| 管理端 | `query_user_state`, `kick_users`, `batch_send_message`, `admin_set_msg_read`, `import_messages`, `admin_transfer_group_owner`, `admin_update_group_attributes`, `admin_remove_group_attributes`, `admin_group_read_receipts` |
|
||||||
| Webhook | `list_webhooks`, `create_webhook`, `update_webhook`, `delete_webhook`, `verify_callback_signature`, `parse_callback_envelope` |
|
| Webhook | `list_webhooks`, `create_webhook`, `update_webhook`, `delete_webhook`, `verify_callback_signature`, `parse_callback_envelope` |
|
||||||
| Push | `register_push_token`, `send_push` |
|
| Push | `register_push_token`, `send_push`, `push_user_status`, `push_device_logs`, `test_offline_push` |
|
||||||
| Update | `check_app_update`, `upload_app_version`, `publish_app_version`, `unpublish_app_version`, `gray_app_version`, `list_app_versions`, `check_rn_update`, `upload_rn_bundle`, `publish_rn_bundle`, `unpublish_rn_bundle`, `list_rn_bundles` |
|
| Update | `check_app_update`, `upload_app_version`, `publish_app_version`, `unpublish_app_version`, `gray_app_version`, `list_app_versions`, `check_rn_update`, `upload_rn_bundle`, `publish_rn_bundle`, `unpublish_rn_bundle`, `list_rn_bundles` |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
@ -7,7 +7,7 @@
|
|||||||
| 系统 | 地址 | 说明 |
|
| 系统 | 地址 | 说明 |
|
||||||
|------|------|------|
|
|------|------|------|
|
||||||
| 租户开放平台 | `https://dev.xuqinmin.com/` | 主账号注册、登录、应用管理 |
|
| 租户开放平台 | `https://dev.xuqinmin.com/` | 主账号注册、登录、应用管理 |
|
||||||
| 运营管理平台 | `https://dev.xuqinmin.com/ops/` | 内部运营管理入口 |
|
| 运营管理平台 | `https://ops.xuqinmin.com/` | 内部运营管理入口 |
|
||||||
|
|
||||||
## 初始化管理员账号
|
## 初始化管理员账号
|
||||||
|
|
||||||
@ -41,9 +41,9 @@
|
|||||||
|
|
||||||
| 路径 | 说明 |
|
| 路径 | 说明 |
|
||||||
|------|------|
|
|------|------|
|
||||||
| `/ops/login` | 运营管理员登录 |
|
| `/login` | 运营管理员登录 |
|
||||||
| `/ops/tenants` | 租户列表 |
|
| `/tenants` | 租户列表 |
|
||||||
| `/ops/statistics` | 统计面板 |
|
| `/statistics` | 统计面板 |
|
||||||
|
|
||||||
## 主要联调接口
|
## 主要联调接口
|
||||||
|
|
||||||
@ -75,4 +75,7 @@
|
|||||||
|
|
||||||
```env
|
```env
|
||||||
VITE_API_BASE_URL=https://dev.xuqinmin.com/api
|
VITE_API_BASE_URL=https://dev.xuqinmin.com/api
|
||||||
|
|
||||||
|
# ops-platform
|
||||||
|
VITE_API_BASE_URL=https://ops.xuqinmin.com/api
|
||||||
```
|
```
|
||||||
|
|||||||
@ -8,7 +8,7 @@ export default defineConfig(({ mode }) => {
|
|||||||
const env = loadEnv(mode, new URL('.', import.meta.url).pathname, '')
|
const env = loadEnv(mode, new URL('.', import.meta.url).pathname, '')
|
||||||
|
|
||||||
return {
|
return {
|
||||||
base: env.VITE_APP_BASE || '/ops/',
|
base: env.VITE_APP_BASE || '/',
|
||||||
plugins: [
|
plugins: [
|
||||||
vue(),
|
vue(),
|
||||||
AutoImport({ resolvers: [ElementPlusResolver()] }),
|
AutoImport({ resolvers: [ElementPlusResolver()] }),
|
||||||
|
|||||||
@ -11,6 +11,8 @@
|
|||||||
"dev:tenant": "yarn workspace tenant-platform dev",
|
"dev:tenant": "yarn workspace tenant-platform dev",
|
||||||
"dev:ops": "yarn workspace ops-platform dev",
|
"dev:ops": "yarn workspace ops-platform dev",
|
||||||
"dev:docs": "yarn workspace docs-site dev",
|
"dev:docs": "yarn workspace docs-site dev",
|
||||||
|
"build:tenant": "yarn workspace tenant-platform build",
|
||||||
|
"build:ops": "yarn workspace ops-platform build",
|
||||||
"build": "yarn workspaces run build"
|
"build": "yarn workspaces run build"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
|||||||
@ -147,13 +147,13 @@ export interface PushServiceConfig {
|
|||||||
export const appApi = {
|
export const appApi = {
|
||||||
list: () => client.get<{ data: App[] }>('/apps'),
|
list: () => client.get<{ data: App[] }>('/apps'),
|
||||||
|
|
||||||
get: (id: string) => client.get<{ data: App }>(`/apps/${id}`),
|
get: (appKey: string) => client.get<{ data: App }>(`/apps/${appKey}`),
|
||||||
|
|
||||||
create: (data: CreateAppRequest) => client.post<{ data: App }>('/apps', data),
|
create: (data: CreateAppRequest) => client.post<{ data: App }>('/apps', data),
|
||||||
|
|
||||||
update: (id: string, data: CreateAppRequest) => client.put<{ data: App }>(`/apps/${id}`, data),
|
update: (appKey: string, data: CreateAppRequest) => client.put<{ data: App }>(`/apps/${appKey}`, data),
|
||||||
|
|
||||||
delete: (id: string) => client.delete(`/apps/${id}`),
|
delete: (appKey: string) => client.delete(`/apps/${appKey}`),
|
||||||
|
|
||||||
getServices: (appKey: string) =>
|
getServices: (appKey: string) =>
|
||||||
client.get<{ data: FeatureService[] }>(`/apps/${appKey}/services`),
|
client.get<{ data: FeatureService[] }>(`/apps/${appKey}/services`),
|
||||||
|
|||||||
@ -72,6 +72,7 @@ export interface ImUser {
|
|||||||
userId: string
|
userId: string
|
||||||
nickname: string
|
nickname: string
|
||||||
avatar?: string
|
avatar?: string
|
||||||
|
admin?: boolean
|
||||||
status: 'ACTIVE' | 'BANNED'
|
status: 'ACTIVE' | 'BANNED'
|
||||||
gender: 'UNKNOWN' | 'MALE' | 'FEMALE'
|
gender: 'UNKNOWN' | 'MALE' | 'FEMALE'
|
||||||
createdAt: number
|
createdAt: number
|
||||||
@ -252,7 +253,7 @@ export const imAdminApi = {
|
|||||||
updateUser(
|
updateUser(
|
||||||
appKey: string,
|
appKey: string,
|
||||||
userId: string,
|
userId: string,
|
||||||
form: { nickname?: string; avatar?: string; gender?: string; status?: string },
|
form: { nickname?: string; avatar?: string; gender?: string; status?: string; admin?: boolean },
|
||||||
) {
|
) {
|
||||||
return imClient.put<{ data: ImUser }>(
|
return imClient.put<{ data: ImUser }>(
|
||||||
`/api/im/admin/users/${encodeURIComponent(userId)}`,
|
`/api/im/admin/users/${encodeURIComponent(userId)}`,
|
||||||
@ -261,6 +262,30 @@ export const imAdminApi = {
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
generateUserSig(
|
||||||
|
appKey: string,
|
||||||
|
userId: string,
|
||||||
|
form?: { expireSeconds?: number; userBuf?: string },
|
||||||
|
) {
|
||||||
|
return imClient.post<{ data: { appKey: string; userId: string; userSig: string; issuedAt: number; expiresAt: number; userBuf?: string } }>(
|
||||||
|
`/api/im/admin/users/${encodeURIComponent(userId)}/usersig`,
|
||||||
|
form ?? {},
|
||||||
|
{ params: { appKey } },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
|
verifyUserSig(
|
||||||
|
appKey: string,
|
||||||
|
userId: string,
|
||||||
|
userSig: string,
|
||||||
|
) {
|
||||||
|
return imClient.post<{ data: { valid: boolean; appKey: string; userId: string; issuedAt: number; expiresAt: number; userBuf?: string } }>(
|
||||||
|
`/api/im/admin/users/${encodeURIComponent(userId)}/usersig/verify`,
|
||||||
|
{ userSig },
|
||||||
|
{ params: { appKey } },
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
updateWebhook(appKey: string, webhookId: string, form: WebhookConfigForm) {
|
updateWebhook(appKey: string, webhookId: string, form: WebhookConfigForm) {
|
||||||
return imClient.put<{ data: WebhookConfig }>(`/api/im/admin/webhooks/${encodeURIComponent(webhookId)}`, form, {
|
return imClient.put<{ data: WebhookConfig }>(`/api/im/admin/webhooks/${encodeURIComponent(webhookId)}`, form, {
|
||||||
params: { appKey },
|
params: { appKey },
|
||||||
@ -444,6 +469,7 @@ export const imAdminApi = {
|
|||||||
avatar?: string,
|
avatar?: string,
|
||||||
gender?: string,
|
gender?: string,
|
||||||
status?: string,
|
status?: string,
|
||||||
|
admin?: boolean,
|
||||||
) {
|
) {
|
||||||
return imClient.post<{ data: ImUser }>(
|
return imClient.post<{ data: ImUser }>(
|
||||||
'/api/im/admin/users',
|
'/api/im/admin/users',
|
||||||
@ -453,6 +479,7 @@ export const imAdminApi = {
|
|||||||
avatar,
|
avatar,
|
||||||
...(gender ? { gender } : {}),
|
...(gender ? { gender } : {}),
|
||||||
...(status ? { status } : {}),
|
...(status ? { status } : {}),
|
||||||
|
...(admin !== undefined ? { admin } : {}),
|
||||||
},
|
},
|
||||||
{ params: { appKey } },
|
{ params: { appKey } },
|
||||||
)
|
)
|
||||||
|
|||||||
@ -33,12 +33,25 @@ function sdkWsUrl() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function parseEvent(message: ImMessage): StoreReviewRefreshEvent | null {
|
function parseEvent(message: ImMessage): StoreReviewRefreshEvent | null {
|
||||||
if (!['NOTIFY', 'CUSTOM'].includes(message.msgType)) return null
|
|
||||||
try {
|
try {
|
||||||
const payload = JSON.parse(message.content) as Partial<StoreReviewRefreshEvent>
|
const raw = JSON.parse(message.content) as Record<string, unknown>
|
||||||
if (!payload || payload.event !== 'store_review_update') return null
|
const payload = (raw?.payload && typeof raw.payload === 'object' ? raw.payload : raw) as Record<string, unknown>
|
||||||
if (!payload.appKey) return null
|
const event = String(payload.event ?? raw?.event ?? '')
|
||||||
return payload as StoreReviewRefreshEvent
|
const appKey = String(payload.appKey ?? raw?.appKey ?? '')
|
||||||
|
if (event !== 'store_review_update' || !appKey) return null
|
||||||
|
return {
|
||||||
|
event,
|
||||||
|
appKey,
|
||||||
|
versionId: String(payload.versionId ?? raw?.versionId ?? ''),
|
||||||
|
storeType: String(payload.storeType ?? raw?.storeType ?? ''),
|
||||||
|
reviewState: String(payload.reviewState ?? raw?.reviewState ?? ''),
|
||||||
|
reviewReason: String(payload.reviewReason ?? raw?.reviewReason ?? ''),
|
||||||
|
stage: String(payload.stage ?? raw?.stage ?? ''),
|
||||||
|
batchId: String(payload.batchId ?? raw?.batchId ?? ''),
|
||||||
|
publishStatus: String(payload.publishStatus ?? raw?.publishStatus ?? ''),
|
||||||
|
source: String(payload.source ?? raw?.source ?? ''),
|
||||||
|
timestamp: Number(payload.timestamp ?? raw?.timestamp ?? Date.now()),
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
@ -52,7 +65,7 @@ export async function connectStoreReviewRealtime(appKey: string, onEvent: (event
|
|||||||
const res = await client.get<{ data: PlatformEventTokenResponse }>('/im/platform-events/token', {
|
const res = await client.get<{ data: PlatformEventTokenResponse }>('/im/platform-events/token', {
|
||||||
params: { appKey },
|
params: { appKey },
|
||||||
})
|
})
|
||||||
const token = res.data.data
|
const token = res.data.data ?? (res.data as unknown as PlatformEventTokenResponse)
|
||||||
init({
|
init({
|
||||||
appKey,
|
appKey,
|
||||||
baseUrl: sdkBaseUrl(),
|
baseUrl: sdkBaseUrl(),
|
||||||
@ -60,6 +73,12 @@ export async function connectStoreReviewRealtime(appKey: string, onEvent: (event
|
|||||||
debug: import.meta.env.DEV,
|
debug: import.meta.env.DEV,
|
||||||
})
|
})
|
||||||
login(token.userId, token.token)
|
login(token.userId, token.token)
|
||||||
|
if (import.meta.env.DEV) {
|
||||||
|
console.debug('[tenant-platform][IM] store review realtime connected token acquired', {
|
||||||
|
appKey,
|
||||||
|
userId: token.userId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const clientInstance = new ImClient()
|
const clientInstance = new ImClient()
|
||||||
clientInstance.on('message', (message) => {
|
clientInstance.on('message', (message) => {
|
||||||
|
|||||||
@ -32,8 +32,9 @@ PushSDK.initialize(userId)</pre>
|
|||||||
<el-col :xs="24" :md="8">
|
<el-col :xs="24" :md="8">
|
||||||
<el-card shadow="hover" class="doc-card">
|
<el-card shadow="hover" class="doc-card">
|
||||||
<template #header>服务端</template>
|
<template #header>服务端</template>
|
||||||
<pre class="code-block">POST /api/auth/login
|
<pre class="code-block">POST /api/im/admin/users/{userId}/usersig
|
||||||
POST /api/im/auth/login
|
POST /api/im/admin/users/{userId}/usersig/verify
|
||||||
|
POST /api/im/auth/login?userSig=...
|
||||||
POST /api/ops/service-requests/{id}/approve</pre>
|
POST /api/ops/service-requests/{id}/approve</pre>
|
||||||
</el-card>
|
</el-card>
|
||||||
</el-col>
|
</el-col>
|
||||||
@ -46,6 +47,13 @@ POST /api/ops/service-requests/{id}/approve</pre>
|
|||||||
<el-step title="开启服务" description="在应用详情开启 IM / Push / Update" />
|
<el-step title="开启服务" description="在应用详情开启 IM / Push / Update" />
|
||||||
<el-step title="接入 SDK" description="按平台文档完成登录和消息能力接入" />
|
<el-step title="接入 SDK" description="按平台文档完成登录和消息能力接入" />
|
||||||
</el-steps>
|
</el-steps>
|
||||||
|
<el-alert
|
||||||
|
title="IM 登录使用 userSig;平台事件账号会使用预先注册的 admin / platform 用户,服务端 SDK / REST API 仅面向管理员账号。"
|
||||||
|
type="info"
|
||||||
|
:closable="false"
|
||||||
|
show-icon
|
||||||
|
style="margin-top:16px"
|
||||||
|
/>
|
||||||
</el-card>
|
</el-card>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|||||||
@ -52,6 +52,12 @@
|
|||||||
</el-tag>
|
</el-tag>
|
||||||
</template>
|
</template>
|
||||||
</el-table-column>
|
</el-table-column>
|
||||||
|
<el-table-column label="权限" width="100">
|
||||||
|
<template #default="{ row }">
|
||||||
|
<el-tag v-if="row.admin" type="warning" size="small">管理员</el-tag>
|
||||||
|
<el-tag v-else type="info" size="small">普通</el-tag>
|
||||||
|
</template>
|
||||||
|
</el-table-column>
|
||||||
<el-table-column label="在线" width="120">
|
<el-table-column label="在线" width="120">
|
||||||
<template #default="{ row }">
|
<template #default="{ row }">
|
||||||
<el-tag v-if="userOnlineState[row.userId]?.online" type="success" size="small">在线</el-tag>
|
<el-tag v-if="userOnlineState[row.userId]?.online" type="success" size="small">在线</el-tag>
|
||||||
@ -71,6 +77,9 @@
|
|||||||
<el-button link type="primary" size="small" @click="openEditUserDialog(row)">
|
<el-button link type="primary" size="small" @click="openEditUserDialog(row)">
|
||||||
编辑
|
编辑
|
||||||
</el-button>
|
</el-button>
|
||||||
|
<el-button link type="primary" size="small" @click="openUserSigDialog(row)">
|
||||||
|
UserSig
|
||||||
|
</el-button>
|
||||||
<el-button
|
<el-button
|
||||||
link
|
link
|
||||||
:type="row.status === 'ACTIVE' ? 'danger' : 'success'"
|
:type="row.status === 'ACTIVE' ? 'danger' : 'success'"
|
||||||
@ -374,6 +383,9 @@
|
|||||||
<el-option label="封禁" value="BANNED" />
|
<el-option label="封禁" value="BANNED" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</el-form-item>
|
</el-form-item>
|
||||||
|
<el-form-item label="管理员">
|
||||||
|
<el-switch v-model="registerForm.admin" inline-prompt active-text="是" inactive-text="否" />
|
||||||
|
</el-form-item>
|
||||||
</el-form>
|
</el-form>
|
||||||
<template #footer>
|
<template #footer>
|
||||||
<el-button @click="showRegisterUser = false">取消</el-button>
|
<el-button @click="showRegisterUser = false">取消</el-button>
|
||||||
@ -716,6 +728,37 @@
|
|||||||
</template>
|
</template>
|
||||||
</el-dialog>
|
</el-dialog>
|
||||||
|
|
||||||
|
<el-dialog v-model="showUserSigDialog" title="UserSig 生成与校验" width="760px" @closed="resetUserSigDialog">
|
||||||
|
<el-descriptions v-if="selectedUserForSig" :column="2" border style="margin-bottom: 16px">
|
||||||
|
<el-descriptions-item label="用户ID">{{ selectedUserForSig.userId }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="权限">{{ selectedUserForSig.admin ? '管理员' : '普通用户' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="状态">{{ selectedUserForSig.status === 'ACTIVE' ? '正常' : '封禁' }}</el-descriptions-item>
|
||||||
|
<el-descriptions-item label="应用">{{ appKey }}</el-descriptions-item>
|
||||||
|
</el-descriptions>
|
||||||
|
<el-form label-width="100px">
|
||||||
|
<el-form-item label="过期秒数">
|
||||||
|
<el-input-number v-model="userSigExpireSeconds" :min="60" :max="315360000" style="width: 240px" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="UserBuf">
|
||||||
|
<el-input v-model="userSigUserBuf" placeholder="可选,用于附加业务信息" />
|
||||||
|
</el-form-item>
|
||||||
|
<el-form-item label="UserSig">
|
||||||
|
<el-input
|
||||||
|
v-model="userSigValue"
|
||||||
|
type="textarea"
|
||||||
|
:rows="6"
|
||||||
|
placeholder="点击生成后这里会显示 UserSig"
|
||||||
|
/>
|
||||||
|
</el-form-item>
|
||||||
|
</el-form>
|
||||||
|
<template #footer>
|
||||||
|
<el-button @click="showUserSigDialog = false">关闭</el-button>
|
||||||
|
<el-button :disabled="!selectedUserForSig" @click="copyUserSig">复制</el-button>
|
||||||
|
<el-button :loading="generatingUserSig" type="primary" @click="generateUserSig">生成</el-button>
|
||||||
|
<el-button :loading="verifyingUserSig" @click="verifyUserSig">校验</el-button>
|
||||||
|
</template>
|
||||||
|
</el-dialog>
|
||||||
|
|
||||||
<el-dialog
|
<el-dialog
|
||||||
v-model="showKeywordFilterDialog"
|
v-model="showKeywordFilterDialog"
|
||||||
:title="editingKeywordFilterId ? '编辑过滤规则' : '新增过滤规则'"
|
:title="editingKeywordFilterId ? '编辑过滤规则' : '新增过滤规则'"
|
||||||
@ -886,8 +929,17 @@ const registerForm = ref({
|
|||||||
avatar: '',
|
avatar: '',
|
||||||
gender: 'UNKNOWN' as 'UNKNOWN' | 'MALE' | 'FEMALE',
|
gender: 'UNKNOWN' as 'UNKNOWN' | 'MALE' | 'FEMALE',
|
||||||
status: 'ACTIVE' as 'ACTIVE' | 'BANNED',
|
status: 'ACTIVE' as 'ACTIVE' | 'BANNED',
|
||||||
|
admin: false,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const showUserSigDialog = ref(false)
|
||||||
|
const selectedUserForSig = ref<ImUser | null>(null)
|
||||||
|
const userSigExpireSeconds = ref(180 * 24 * 60 * 60)
|
||||||
|
const userSigUserBuf = ref('')
|
||||||
|
const userSigValue = ref('')
|
||||||
|
const generatingUserSig = ref(false)
|
||||||
|
const verifyingUserSig = ref(false)
|
||||||
|
|
||||||
const showCreateGroup = ref(false)
|
const showCreateGroup = ref(false)
|
||||||
const submittingCreateGroup = ref(false)
|
const submittingCreateGroup = ref(false)
|
||||||
const createGroupForm = ref({
|
const createGroupForm = ref({
|
||||||
@ -1033,6 +1085,7 @@ function openCreateUserDialog() {
|
|||||||
avatar: '',
|
avatar: '',
|
||||||
gender: 'UNKNOWN',
|
gender: 'UNKNOWN',
|
||||||
status: 'ACTIVE',
|
status: 'ACTIVE',
|
||||||
|
admin: false,
|
||||||
}
|
}
|
||||||
showRegisterUser.value = true
|
showRegisterUser.value = true
|
||||||
}
|
}
|
||||||
@ -1045,6 +1098,7 @@ function openEditUserDialog(user: ImUser) {
|
|||||||
avatar: user.avatar ?? '',
|
avatar: user.avatar ?? '',
|
||||||
gender: user.gender ?? 'UNKNOWN',
|
gender: user.gender ?? 'UNKNOWN',
|
||||||
status: user.status ?? 'ACTIVE',
|
status: user.status ?? 'ACTIVE',
|
||||||
|
admin: Boolean(user.admin),
|
||||||
}
|
}
|
||||||
showRegisterUser.value = true
|
showRegisterUser.value = true
|
||||||
}
|
}
|
||||||
@ -1057,9 +1111,71 @@ function resetUserForm() {
|
|||||||
avatar: '',
|
avatar: '',
|
||||||
gender: 'UNKNOWN',
|
gender: 'UNKNOWN',
|
||||||
status: 'ACTIVE',
|
status: 'ACTIVE',
|
||||||
|
admin: false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function openUserSigDialog(user: ImUser) {
|
||||||
|
selectedUserForSig.value = user
|
||||||
|
userSigExpireSeconds.value = 180 * 24 * 60 * 60
|
||||||
|
userSigUserBuf.value = ''
|
||||||
|
userSigValue.value = ''
|
||||||
|
showUserSigDialog.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetUserSigDialog() {
|
||||||
|
selectedUserForSig.value = null
|
||||||
|
userSigExpireSeconds.value = 180 * 24 * 60 * 60
|
||||||
|
userSigUserBuf.value = ''
|
||||||
|
userSigValue.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
async function generateUserSig() {
|
||||||
|
if (!selectedUserForSig.value) return
|
||||||
|
generatingUserSig.value = true
|
||||||
|
try {
|
||||||
|
const res = await imAdminApi.generateUserSig(appKey.value, selectedUserForSig.value.userId, {
|
||||||
|
expireSeconds: userSigExpireSeconds.value,
|
||||||
|
userBuf: userSigUserBuf.value.trim() || undefined,
|
||||||
|
})
|
||||||
|
userSigValue.value = res.data.data.userSig
|
||||||
|
ElMessage.success('UserSig 已生成')
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('UserSig 生成失败')
|
||||||
|
} finally {
|
||||||
|
generatingUserSig.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function verifyUserSig() {
|
||||||
|
if (!selectedUserForSig.value || !userSigValue.value.trim()) {
|
||||||
|
ElMessage.warning('请先生成或粘贴 UserSig')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
verifyingUserSig.value = true
|
||||||
|
try {
|
||||||
|
const res = await imAdminApi.verifyUserSig(appKey.value, selectedUserForSig.value.userId, userSigValue.value.trim())
|
||||||
|
if (res.data.data.valid) {
|
||||||
|
ElMessage.success('UserSig 校验通过')
|
||||||
|
} else {
|
||||||
|
ElMessage.warning('UserSig 校验未通过')
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('UserSig 校验失败')
|
||||||
|
} finally {
|
||||||
|
verifyingUserSig.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function copyUserSig() {
|
||||||
|
if (!userSigValue.value.trim()) {
|
||||||
|
ElMessage.warning('没有可复制的 UserSig')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
await navigator.clipboard.writeText(userSigValue.value.trim())
|
||||||
|
ElMessage.success('已复制 UserSig')
|
||||||
|
}
|
||||||
|
|
||||||
function openCreateGroupDialog() {
|
function openCreateGroupDialog() {
|
||||||
resetCreateGroupForm()
|
resetCreateGroupForm()
|
||||||
showCreateGroup.value = true
|
showCreateGroup.value = true
|
||||||
@ -1690,6 +1806,7 @@ async function submitRegisterUser() {
|
|||||||
avatar: registerForm.value.avatar.trim() || undefined,
|
avatar: registerForm.value.avatar.trim() || undefined,
|
||||||
gender: registerForm.value.gender,
|
gender: registerForm.value.gender,
|
||||||
status: registerForm.value.status,
|
status: registerForm.value.status,
|
||||||
|
admin: registerForm.value.admin,
|
||||||
}
|
}
|
||||||
if (editingUserId.value) {
|
if (editingUserId.value) {
|
||||||
await imAdminApi.updateUser(appKey.value, editingUserId.value, payload)
|
await imAdminApi.updateUser(appKey.value, editingUserId.value, payload)
|
||||||
@ -1702,6 +1819,7 @@ async function submitRegisterUser() {
|
|||||||
payload.avatar,
|
payload.avatar,
|
||||||
payload.gender,
|
payload.gender,
|
||||||
payload.status,
|
payload.status,
|
||||||
|
payload.admin,
|
||||||
)
|
)
|
||||||
ElMessage.success('用户注册成功')
|
ElMessage.success('用户注册成功')
|
||||||
}
|
}
|
||||||
|
|||||||
@ -551,7 +551,7 @@ async function saveConfig() {
|
|||||||
},
|
},
|
||||||
profiles: pushConfig.profiles.map(profile => normalizeProfile(profile)),
|
profiles: pushConfig.profiles.map(profile => normalizeProfile(profile)),
|
||||||
}
|
}
|
||||||
await appApi.updateServiceConfig(app.value.id, selectedPlatform.value, 'PUSH', payload)
|
await appApi.updateServiceConfig(app.value.appKey, selectedPlatform.value, 'PUSH', payload)
|
||||||
ElMessage.success('推送配置已保存')
|
ElMessage.success('推送配置已保存')
|
||||||
await loadData()
|
await loadData()
|
||||||
} catch {
|
} catch {
|
||||||
@ -580,7 +580,7 @@ async function onTogglePushService(enable: boolean) {
|
|||||||
cancelButtonText: '取消',
|
cancelButtonText: '取消',
|
||||||
})
|
})
|
||||||
if (!app.value) return
|
if (!app.value) return
|
||||||
await appApi.toggleService(app.value.id, selectedPlatform.value, 'PUSH', false)
|
await appApi.toggleService(app.value.appKey, selectedPlatform.value, 'PUSH', false)
|
||||||
ElMessage.success('已关闭')
|
ElMessage.success('已关闭')
|
||||||
await loadData()
|
await loadData()
|
||||||
}
|
}
|
||||||
|
|||||||
@ -142,7 +142,7 @@ async function sendVerifyCode() {
|
|||||||
if (!selectedApp.value) return
|
if (!selectedApp.value) return
|
||||||
sendingCode.value = true
|
sendingCode.value = true
|
||||||
try {
|
try {
|
||||||
await appApi.requestSecretVerify(selectedApp.value.id, dialogMode.value)
|
await appApi.requestSecretVerify(selectedApp.value.appKey, dialogMode.value)
|
||||||
codeSent.value = true
|
codeSent.value = true
|
||||||
ElMessage.success('验证码已发送到邮箱')
|
ElMessage.success('验证码已发送到邮箱')
|
||||||
} finally {
|
} finally {
|
||||||
@ -159,10 +159,10 @@ async function submitVerify() {
|
|||||||
submitting.value = true
|
submitting.value = true
|
||||||
try {
|
try {
|
||||||
if (dialogMode.value === 'REVEAL_SECRET') {
|
if (dialogMode.value === 'REVEAL_SECRET') {
|
||||||
const res = await appApi.revealSecret(selectedApp.value.id, verifyCode.value.trim())
|
const res = await appApi.revealSecret(selectedApp.value.appKey, verifyCode.value.trim())
|
||||||
secretResult.value = res.data.data.appSecret
|
secretResult.value = res.data.data.appSecret
|
||||||
} else {
|
} else {
|
||||||
const res = await appApi.resetSecret(selectedApp.value.id, verifyCode.value.trim())
|
const res = await appApi.resetSecret(selectedApp.value.appKey, verifyCode.value.trim())
|
||||||
secretResult.value = res.data.data.appSecret
|
secretResult.value = res.data.data.appSecret
|
||||||
}
|
}
|
||||||
showDialog.value = false
|
showDialog.value = false
|
||||||
|
|||||||
@ -110,10 +110,10 @@ const STORE_GUIDES: StoreGuide[] = [
|
|||||||
urlLabel: '查看小米官方文档',
|
urlLabel: '查看小米官方文档',
|
||||||
steps: [
|
steps: [
|
||||||
{ title: '进入应用管理', description: '定位到目标应用。' },
|
{ title: '进入应用管理', description: '定位到目标应用。' },
|
||||||
{ title: '准备自动发布接口密钥', description: '记录用户名和 RSA 私钥。' },
|
{ title: '准备自动发布接口密钥', description: '记录用户名、公钥证书和 RSA 私钥。' },
|
||||||
{ title: '保存到租户平台', description: '完成后可提交审核。' },
|
{ title: '保存到租户平台', description: '完成后可提交审核。' },
|
||||||
],
|
],
|
||||||
hint: '字段按 username / privateKey 保存。',
|
hint: '字段按 username / publicKey / privateKey 保存。',
|
||||||
image: miGuideImage,
|
image: miGuideImage,
|
||||||
enabled: true,
|
enabled: true,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -8,7 +8,7 @@
|
|||||||
<div v-if="isServicesPortal" class="portal-bar">
|
<div v-if="isServicesPortal" class="portal-bar">
|
||||||
<span class="portal-bar-title">版本管理</span>
|
<span class="portal-bar-title">版本管理</span>
|
||||||
<el-select :model-value="appKey" placeholder="选择应用" style="width:220px" @change="switchApp">
|
<el-select :model-value="appKey" placeholder="选择应用" style="width:220px" @change="switchApp">
|
||||||
<el-option v-for="a in portalApps" :key="a.id" :label="a.name" :value="a.id" />
|
<el-option v-for="a in portalApps" :key="a.appKey" :label="a.name" :value="a.appKey" />
|
||||||
</el-select>
|
</el-select>
|
||||||
</div>
|
</div>
|
||||||
<el-page-header v-else @back="$router.back()" :content="`版本管理 — ${pageTitle}`" style="margin-bottom:20px" />
|
<el-page-header v-else @back="$router.back()" :content="`版本管理 — ${pageTitle}`" style="margin-bottom:20px" />
|
||||||
@ -778,7 +778,12 @@ import {
|
|||||||
type StoreConfig,
|
type StoreConfig,
|
||||||
type StoreType,
|
type StoreType,
|
||||||
} from '@/api/update'
|
} from '@/api/update'
|
||||||
import { connectStoreReviewRealtime, disconnectStoreReviewRealtime } from '@/services/storeReviewRealtime'
|
import {
|
||||||
|
connectStoreReviewRealtime,
|
||||||
|
disconnectStoreReviewRealtime,
|
||||||
|
notifyStoreReviewRefresh,
|
||||||
|
type StoreReviewRefreshEvent,
|
||||||
|
} from '@/services/storeReviewRealtime'
|
||||||
import huaweiGuideImage from '@/assets/update-store/huawei/01.png'
|
import huaweiGuideImage from '@/assets/update-store/huawei/01.png'
|
||||||
import miGuideImage from '@/assets/update-store/mi/01.png'
|
import miGuideImage from '@/assets/update-store/mi/01.png'
|
||||||
import oppoGuideImage from '@/assets/update-store/oppo/01.png'
|
import oppoGuideImage from '@/assets/update-store/oppo/01.png'
|
||||||
@ -1975,7 +1980,15 @@ onMounted(() => {
|
|||||||
loadStoreConfigs()
|
loadStoreConfigs()
|
||||||
loadPublishConfig()
|
loadPublishConfig()
|
||||||
loadOperationLogs()
|
loadOperationLogs()
|
||||||
void connectStoreReviewRealtime(appKey, () => {
|
void connectStoreReviewRealtime(appKey, (event: StoreReviewRefreshEvent) => {
|
||||||
|
notifyStoreReviewRefresh(event.appKey)
|
||||||
|
const storeName = storeLabel(event.storeType || '') || event.storeType || '应用市场'
|
||||||
|
const stateName = reviewLabel((event.reviewState || '').toUpperCase()) || event.reviewState || '状态变更'
|
||||||
|
ElMessage.success(
|
||||||
|
event.reviewReason
|
||||||
|
? `${storeName} 审核状态更新为 ${stateName}:${event.reviewReason}`
|
||||||
|
: `${storeName} 审核状态更新为 ${stateName}`,
|
||||||
|
)
|
||||||
scheduleStoreReviewReload()
|
scheduleStoreReviewReload()
|
||||||
}).catch((error) => {
|
}).catch((error) => {
|
||||||
if (import.meta.env.DEV) {
|
if (import.meta.env.DEV) {
|
||||||
|
|||||||
正在加载...
在新工单中引用
屏蔽一个用户