574 lines
16 KiB
Markdown
574 lines
16 KiB
Markdown
# IM_NEW 接口文档
|
||
|
||
> 源码基线:`IM_NEW/codex/api-alignment-fixes`(起点 `32177a7293ec9eb7bd731467953da3c51e561beb`)
|
||
> 文档以控制器、DTO、枚举、序列化配置和 Nginx 网关配置为准;“当前实现注意事项”用于标明源码中的实际限制,不代表理想设计。
|
||
|
||
## 1. 通用约定
|
||
|
||
### 1.1 网关与路径大小写
|
||
|
||
- 网关默认端口:`8009`。
|
||
- HTTP API 前缀:`/api`。
|
||
- SignalR Hub:`/chat`。
|
||
- Nginx `location` 匹配区分大小写,因此应使用本文给出的全小写控制器前缀,例如 `/api/user/me`。`/api/User/Me` 在当前网关会返回 404。
|
||
|
||
### 1.2 认证
|
||
|
||
除登录、注册以及下文特别标注的接口外,请携带:
|
||
|
||
```http
|
||
Authorization: Bearer <JWT>
|
||
```
|
||
|
||
SignalR 客户端通过 `/chat?access_token=<JWT>` 完成握手;使用官方 SignalR 客户端时由 `accessTokenFactory` 自动添加。
|
||
|
||
Token 缺失、无效或过期时返回 **HTTP 401**,业务响应为:
|
||
|
||
```json
|
||
{ "code": 1006, "message": "认证失败", "data": null }
|
||
```
|
||
|
||
已认证但权限不足时返回 **HTTP 403**,业务码为 `1005`。新版前端在滚动发布期间仍兼容旧服务的 HTTP 200 + `code=1006`。
|
||
|
||
### 1.3 统一响应
|
||
|
||
除文件内容下载外,接口通常返回:
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"message": "成功",
|
||
"data": {}
|
||
}
|
||
```
|
||
|
||
- JSON 属性使用 camelCase。
|
||
- `code === 0` 表示业务成功。
|
||
- 参数校验失败也返回 HTTP 200,业务码为 `1003`。
|
||
- 枚举通过 `JsonStringEnumConverter` 序列化为字符串,并接受字符串枚举请求值。
|
||
- 未处理异常统一返回 HTTP 500 + `code=1000`,响应头 `X-Correlation-ID` 可用于关联服务端日志。
|
||
|
||
## 2. 认证与用户
|
||
|
||
### 2.1 POST `/api/auth/login`
|
||
|
||
无需认证。请求:
|
||
|
||
```json
|
||
{ "userName": "user1", "password": "******" }
|
||
```
|
||
|
||
校验:`userName` 5–20 字符;`password` 非空且不超过 50 字符。
|
||
|
||
成功返回 `Result<LoginResponse>`:
|
||
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"message": "成功",
|
||
"data": {
|
||
"userId": "guid",
|
||
"token": "jwt",
|
||
"refreshToken": "string",
|
||
"expired": null,
|
||
"userName": "string",
|
||
"nickName": "string",
|
||
"avatar": "string|null",
|
||
"creationTime": "datetime-offset"
|
||
}
|
||
}
|
||
```
|
||
|
||
当前实现创建 `LoginResponse` 时将 `expired` 传为 `null`。
|
||
|
||
### 2.2 POST `/api/auth/register`
|
||
|
||
无需认证。请求:
|
||
|
||
```json
|
||
{ "userName": "newuser", "password": "******", "nickName": "新用户" }
|
||
```
|
||
|
||
校验:`userName` 5–20 字符;`password` 6–50 字符;`nickName` 非空且不超过 50 字符。返回 `Result<UserResponse>`。
|
||
|
||
### 2.3 POST `/api/auth/refresh`
|
||
|
||
无需 Access Token。请求:
|
||
|
||
```json
|
||
{ "refreshToken": "string" }
|
||
```
|
||
|
||
返回新的 `Result<LoginResponse>`。
|
||
|
||
### 2.4 用户接口
|
||
|
||
| 方法 | 路径 | 参数 | 返回 |
|
||
|---|---|---|---|
|
||
| GET | `/api/user/me` | 无 | `Result<UserResponse>` |
|
||
| GET | `/api/user/find?userId={guid}` | query: `userId` | `Result<UserResponse>` |
|
||
| GET | `/api/user/findByUname?username={value}` | query: `username` | `Result<UserResponse>` |
|
||
| POST | `/api/user/update` | body: `UserUpdateRequest` | `Result<UserResponse>` |
|
||
| POST | `/api/user/getUsersByIds` | body: GUID 数组 | `Result<UserResponse[]>` |
|
||
|
||
`UserUpdateRequest` 的字段均可省略:
|
||
|
||
```json
|
||
{
|
||
"nickName": "string|null",
|
||
"region": "string|null",
|
||
"avatar": "string|null",
|
||
"description": "string|null"
|
||
}
|
||
```
|
||
|
||
`UserResponse`:
|
||
|
||
```json
|
||
{
|
||
"id": "guid",
|
||
"userName": "string",
|
||
"nickName": "string",
|
||
"email": "string|null",
|
||
"phone": "string|null",
|
||
"region": "string",
|
||
"description": "string",
|
||
"avatar": "string|null",
|
||
"creationTime": "datetime-offset",
|
||
"deletion": "datetime-offset|null"
|
||
}
|
||
```
|
||
|
||
## 3. 好友与好友申请
|
||
|
||
所有接口均要求认证。
|
||
|
||
### 3.1 好友
|
||
|
||
| 方法 | 路径 | 参数 | 返回 |
|
||
|---|---|---|---|
|
||
| GET | `/api/friend/list` | 无 | `Result<FriendResponse[]>` |
|
||
| POST | `/api/friend/delete?friendId={guid}` | 好友关系记录 ID,不是对方用户 ID | `Result<object>`,成功时 `data: null` |
|
||
| POST | `/api/friend/block?friendId={guid}` | 好友关系记录 ID | `Result<object>` |
|
||
| GET | `/api/friend/checkFriend?userId={guid}&targetId={guid}` | 两个用户 ID | `Result<boolean>` |
|
||
|
||
`FriendResponse`:
|
||
|
||
```json
|
||
{
|
||
"id": "friend-relation-guid",
|
||
"targetId": "peer-user-guid",
|
||
"avatar": "string|null",
|
||
"nickName": "string",
|
||
"remarkName": "string|null",
|
||
"createTime": "datetime",
|
||
"updateTime": "datetime|null",
|
||
"status": "Pending|Added|Declined|Blocked"
|
||
}
|
||
```
|
||
|
||
### 3.2 好友申请
|
||
|
||
#### POST `/api/friendRequest/add`
|
||
|
||
```json
|
||
{
|
||
"targetId": "guid",
|
||
"description": "string|null",
|
||
"remarkName": "string|null"
|
||
}
|
||
```
|
||
|
||
`targetId` 必填。返回 `Result<FriendRequestResponse>`。
|
||
|
||
#### POST `/api/friendRequest/handle`
|
||
|
||
```json
|
||
{
|
||
"requestId": "guid",
|
||
"action": "Accpet|Reject|Block",
|
||
"remarkName": "string|null"
|
||
}
|
||
```
|
||
|
||
注意:后端枚举当前拼写为 `Accpet`,接受申请时 `remarkName` 必填。
|
||
|
||
#### GET `/api/friendRequest/list`
|
||
|
||
返回与当前用户相关的申请:`Result<FriendRequestResponse[]>`。
|
||
|
||
`FriendRequestResponse.state`:`Pending`、`Declined`、`Passed`、`Blocked`。
|
||
|
||
## 4. 群组
|
||
|
||
### 4.1 群信息
|
||
|
||
| 方法 | 路径 | 请求 | 返回 |
|
||
|---|---|---|---|
|
||
| GET | `/api/group/getAll` | 无 | `Result<GroupResponse[]>` |
|
||
| GET | `/api/group/getOne?groupId={guid}` | query: `groupId` | `Result<GroupResponse>` |
|
||
| POST | `/api/group/create` | `{ "name": "string|null" }` | `Result<GroupResponse>` |
|
||
| POST | `/api/group/update` | `GroupUpdateRequest` | `Result<GroupResponse>` |
|
||
| POST | `/api/group/dissolve?groupId={guid}` | 群 ID;仅群主 | `Result<object>` |
|
||
|
||
`name` 最大 20 字符。更新请求:
|
||
|
||
```json
|
||
{
|
||
"groupId": "guid",
|
||
"groupName": "string|null",
|
||
"avatar": "string|null",
|
||
"description": "string|null"
|
||
}
|
||
```
|
||
|
||
`description` 实际用于更新群公告 `announcement`。
|
||
|
||
`GroupResponse`:
|
||
|
||
```json
|
||
{
|
||
"id": "guid",
|
||
"name": "string",
|
||
"groupMaster": "guid",
|
||
"authority": "REQUIRE_CONSENT|ANYONE_CAN_JOIN|NOT_ALLOWED_TO_JOIN",
|
||
"allMembersBanned": false,
|
||
"status": "Normal|Blocked",
|
||
"announcement": "string",
|
||
"avatar": "string|null",
|
||
"maxSequenceId": 0,
|
||
"lastMessage": "string",
|
||
"lastSenderName": "string",
|
||
"created": "datetime-offset",
|
||
"updated": "datetime-offset"
|
||
}
|
||
```
|
||
|
||
`getAll` 按当前用户的有效群成员关系返回群,普通成员、管理员和群主均可看到已加入群。
|
||
|
||
### 4.2 群成员
|
||
|
||
| 方法 | 路径 | 参数 | 返回 |
|
||
|---|---|---|---|
|
||
| GET | `/api/groupMember/checkMember?userId={guid}&groupId={guid}` | 用户 ID、群 ID | `Result<boolean>` |
|
||
| GET | `/api/groupMember/list?groupId={guid}` | 群 ID | `Result<GroupMemberResponse[]>` |
|
||
| POST | `/api/groupMember/delete?memberId={guid}` | 群成员记录 ID | `Result<object>` |
|
||
| POST | `/api/groupMember/leave?groupId={guid}` | 群 ID | `Result<object>` |
|
||
|
||
`GroupMemberResponse.role`:`Normal`、`Administrator`、`Master`。
|
||
|
||
`list`、`delete`、`leave` 均要求登录。`delete` 是管理操作,只能移除角色低于操作者的其他成员,禁止移除群主或自己;群主必须使用 `dissolve`,不能使用 `leave`。
|
||
|
||
`checkMember` 是服务间内部接口,必须携带 `X-Internal-Api-Key`,不应通过公网网关暴露。
|
||
|
||
### 4.3 群邀请
|
||
|
||
| 方法 | 路径 | 参数 | 返回 |
|
||
|---|---|---|---|
|
||
| POST | `/api/groupInvitation/send` | body: `{ "groupId": "guid", "userId": "guid" }` | `Result<GroupInvitationResponse>` |
|
||
| GET | `/api/groupInvitation/get?invitationId={guid}` | 邀请 ID | `Result<GroupInvitationResponse>` |
|
||
| POST | `/api/groupInvitation/handle?invitationId={guid}&action={value}` | `action=Accept|Reject` | `Result<object>` |
|
||
|
||
邀请状态:`Pending`、`Passed`、`Reject`。
|
||
|
||
### 4.4 入群申请
|
||
|
||
| 方法 | 路径 | 参数 | 返回 |
|
||
|---|---|---|---|
|
||
| POST | `/api/groupRequest/send` | body: `{ "groupId": "guid", "desc": "string|null" }` | `Result<GroupRequestResponse>` |
|
||
| POST | `/api/groupRequest/handle?requestId={guid}&action={value}` | `action=Accept|Reject` | `Result<object>` |
|
||
| GET | `/api/groupRequest/find?id={guid}` | 申请 ID | `Result<GroupRequestResponse>` |
|
||
| GET | `/api/groupRequest/list` | 无 | `Result<GroupRequestResponse[]>` |
|
||
|
||
`desc` 最大 20 字符;状态为 `Pending`、`Declined` 或 `Passed`。
|
||
|
||
`list` 返回当前用户提交的申请,以及当前用户作为管理员或群主有权处理的群申请。
|
||
|
||
## 5. 会话与消息
|
||
|
||
所有接口均要求认证。
|
||
|
||
### 5.1 会话
|
||
|
||
| 方法 | 路径 | 参数 | 返回 |
|
||
|---|---|---|---|
|
||
| GET | `/api/conversation/list` | 无 | `Result<ConversationResponse[]>` |
|
||
| GET | `/api/conversation/get?id={guid}` | 会话 ID | `Result<ConversationResponse>` |
|
||
| POST | `/api/conversation/markRead?conversationId={guid}` | 会话 ID | `Result<object>` |
|
||
|
||
`ConversationResponse`:
|
||
|
||
```json
|
||
{
|
||
"id": "guid",
|
||
"userId": "guid",
|
||
"targetId": "guid",
|
||
"targetAvatar": "string",
|
||
"targetName": "string",
|
||
"lastReadSequenceId": 0,
|
||
"unreadCount": 0,
|
||
"chatType": "PRIVATE|GROUP",
|
||
"lastMessage": "string",
|
||
"dateTime": "datetime"
|
||
}
|
||
```
|
||
|
||
### 5.2 POST `/api/message/send`
|
||
|
||
```json
|
||
{
|
||
"clientMsgId": "guid",
|
||
"targetId": "guid",
|
||
"chatType": "PRIVATE|GROUP",
|
||
"msgType": "Text|Image|Voice|Video|File|VoiceChat|VideoChat",
|
||
"quoteMessageId": "guid|null",
|
||
"ext": { "key": "value" },
|
||
"text": "string|null",
|
||
"url": "string|null",
|
||
"width": 0,
|
||
"height": 0,
|
||
"thumb": "string|null",
|
||
"duration": 0,
|
||
"fileId": "guid|null",
|
||
"fileName": "string|null",
|
||
"fileSize": 0,
|
||
"fileFormat": "mime/type|null"
|
||
}
|
||
```
|
||
|
||
- `clientMsgId`、`targetId` 必填。
|
||
- `Text` 要求 `text`。
|
||
- `Image`、`Video`、`Voice` 至少提供 `url` 或 `fileId`。
|
||
- `File` 要求 `fileId`、`fileName`、`fileSize`、`fileFormat`;服务端已支持构建和保存文件消息。
|
||
- `VoiceChat`、`VideoChat` 仍未实现,发送会返回 `2303`。
|
||
|
||
成功返回 `Result<MessageResponse>`:
|
||
|
||
```json
|
||
{
|
||
"id": "guid",
|
||
"clientMsgId": "guid",
|
||
"chatType": "PRIVATE",
|
||
"msgType": "Text",
|
||
"senderId": "guid",
|
||
"targetId": "guid",
|
||
"state": "Sent|Withdrwan",
|
||
"streamKey": "string",
|
||
"sequenceId": 1,
|
||
"creationTime": "datetime-offset",
|
||
"content": {
|
||
"fallback": "string",
|
||
"body": {},
|
||
"ext": {},
|
||
"quote": null
|
||
}
|
||
}
|
||
```
|
||
|
||
### 5.3 其他消息接口
|
||
|
||
| 方法 | 路径 | 参数 | 返回 |
|
||
|---|---|---|---|
|
||
| POST | `/api/message/withDraw?msgId={guid}` | 消息 ID | `Result<object>` |
|
||
| GET | `/api/message/getMessages?conversationId={guid}&cursor={long?}&direction={int}&limit={int}` | 会话、游标、方向、条数 | `Result<GetMessagesResponse>` |
|
||
|
||
`GetMessagesResponse`:
|
||
|
||
```json
|
||
{ "messages": [], "hasmore": false }
|
||
```
|
||
|
||
方向约定:`0` 查询 `SequenceId < cursor` 的历史消息;`1` 查询 `SequenceId > cursor` 的增量消息。两个方向都查询 `limit + 1` 条判断 `hasmore`,但响应最多返回 `limit` 条并按序列号升序;`limit` 范围为 1–100。
|
||
|
||
## 6. 文件服务
|
||
|
||
所有接口均要求认证。
|
||
|
||
### 6.1 文件
|
||
|
||
#### POST `/api/file/simple-upload`
|
||
|
||
`multipart/form-data`:
|
||
|
||
| 字段 | 类型 | 必填 |
|
||
|---|---|---|
|
||
| `file` | 文件 | 是 |
|
||
| `isPublic` | boolean | 是 |
|
||
|
||
服务端计算 MD5 并执行安全秒传:公开文件可复用,私有文件只允许同一所有者复用。返回 `Result<FileResponse>`。
|
||
|
||
#### GET `/api/file/{id}`
|
||
|
||
返回文件信息 `Result<FileResponse>`。
|
||
|
||
#### GET `/api/file/{id}/content`
|
||
|
||
返回鉴权后的二进制文件流,支持 HTTP Range,不使用 `Result<T>` 包装。无权限返回 HTTP 403,文件不存在返回 HTTP 404。
|
||
|
||
`FileResponse` 为扁平结构,不暴露存储桶、对象键等内部位置:
|
||
|
||
```json
|
||
{
|
||
"id": "guid",
|
||
"ownerId": "guid",
|
||
"fileName": "avatar.png",
|
||
"fileSize": 123,
|
||
"contentType": "image/png",
|
||
"state": "Uploaded",
|
||
"checkSum": "md5-hex",
|
||
"chatType": "PRIVATE|GROUP|null",
|
||
"targetId": "guid|null",
|
||
"isPublic": false,
|
||
"created": "datetime-offset",
|
||
"updated": "datetime-offset",
|
||
"url": "string|null"
|
||
}
|
||
```
|
||
|
||
`url` 仅在存储提供方能够生成公开地址时存在。
|
||
|
||
### 6.2 分片上传
|
||
|
||
#### POST `/api/fileTask/init`
|
||
|
||
```json
|
||
{
|
||
"conversationId": "guid",
|
||
"chatType": "PRIVATE|GROUP",
|
||
"targetId": "peer-or-group-guid",
|
||
"fileName": "string",
|
||
"fileSize": 123,
|
||
"contentType": "mime/type",
|
||
"checkSum": "md5"
|
||
}
|
||
```
|
||
|
||
返回:
|
||
|
||
```json
|
||
{
|
||
"taskId": "guid",
|
||
"uploadSessionId": "string",
|
||
"instant": false,
|
||
"uploadMode": "LocalMultipart|Presigned",
|
||
"totalPartCount": 1,
|
||
"partSizeBytes": 5242880,
|
||
"file": null
|
||
}
|
||
```
|
||
|
||
秒传命中时返回 `instant: true`、`uploadMode: "Instant"` 和最终 `file`;客户端直接使用该文件,不再调用 `complete`。
|
||
|
||
#### GET `/api/fileTask/getuploadurl?sessionId={value}&partNum={n}`
|
||
|
||
返回 `Result<PresignedUrl>`,不是字符串:
|
||
|
||
```json
|
||
{
|
||
"url": "string",
|
||
"method": "PUT|POST",
|
||
"headers": {},
|
||
"expiresAt": "datetime-offset"
|
||
}
|
||
```
|
||
|
||
#### GET `/api/fileTask/progress?sessionId={value}`
|
||
|
||
```json
|
||
{
|
||
"sessionId": "string",
|
||
"taskId": "string",
|
||
"fileSize": 123,
|
||
"totalPartCount": 1,
|
||
"completedPartCount": 0,
|
||
"uploadedBytes": 0,
|
||
"progressPercent": 0
|
||
}
|
||
```
|
||
|
||
#### POST `/api/fileTask/local/parts/upload`
|
||
|
||
`multipart/form-data`:`sessionId`、`partNumber`、`file`。返回 `Result<CompleteUploadResult>`,其中包含 `location`、`eTag`、`size`、`checksum` 和 `versionId`。
|
||
|
||
#### POST `/api/fileTask/complete`
|
||
|
||
```json
|
||
{
|
||
"sessionId": "string",
|
||
"parts": [
|
||
{ "partNumber": 1, "eTag": "string", "size": 123, "checksum": null }
|
||
]
|
||
}
|
||
```
|
||
|
||
成功接受异步合并时返回 HTTP 202 + `Result<UploadTaskResponse>`,其中 `state` 为 `Merging`。
|
||
|
||
#### GET `/api/fileTask/status?taskId={guid}`
|
||
|
||
客户端轮询此接口。处理中返回 `Uploading|Merging`;失败返回 `state: "Failed"` 与 `failureReason`;完成后返回:
|
||
|
||
```json
|
||
{
|
||
"id": "upload-task-guid",
|
||
"uploaderId": "guid",
|
||
"conversationId": "guid",
|
||
"fileName": "string",
|
||
"fileSize": 123,
|
||
"contentType": "mime/type",
|
||
"storageLocation": {},
|
||
"state": "Completed",
|
||
"checkSum": "string",
|
||
"resultFileId": "guid",
|
||
"failureReason": null,
|
||
"file": { "id": "guid", "fileName": "string", "fileSize": 123, "url": null }
|
||
}
|
||
```
|
||
|
||
任务、分片、进度、完成和状态接口都会校验当前用户是上传者。私有文件的 `url` 为 null,使用 `/api/file/{id}/content` 鉴权读取。
|
||
|
||
## 7. SignalR
|
||
|
||
- Hub:`/chat`
|
||
- 服务端事件:`ReceiveNewMessage`
|
||
- 当前 Hub 没有 `clearUnreadCount` 方法;清零未读应调用 HTTP `/api/conversation/markRead`。
|
||
|
||
推送载荷与 HTTP `MessageResponse` 略有不同:
|
||
|
||
```json
|
||
{
|
||
"id": "guid",
|
||
"clientId": "guid",
|
||
"chatType": "PRIVATE|GROUP",
|
||
"msgType": "Text|Image|Voice|Video",
|
||
"senderId": "guid",
|
||
"targetId": "guid",
|
||
"state": "Sent|Withdrwan",
|
||
"streamKey": "string",
|
||
"sequenceId": 1,
|
||
"pushTimestamp": 0,
|
||
"content": {
|
||
"fallback": "string",
|
||
"body": {},
|
||
"ext": {},
|
||
"quote": null
|
||
}
|
||
}
|
||
```
|
||
|
||
## 8. 业务状态码
|
||
|
||
| 范围/代码 | 含义 |
|
||
|---|---|
|
||
| `0` | 成功 |
|
||
| `1000`–`1006` | 系统、超时、参数、数据库、权限、认证错误 |
|
||
| `2000`–`2004` | 用户不存在、已存在、密码错误、禁用、登录过期 |
|
||
| `2100`–`2107` | 好友申请、好友关系和操作错误 |
|
||
| `2200`–`2206` | 群不存在、已入群、群满、权限、邀请、申请、成员错误 |
|
||
| `2300`–`2303` | 消息发送、消息不存在、撤回、不支持的消息类型 |
|
||
| `2400`–`2403` | 文件上传、不存在、过大、类型不支持 |
|
||
| `3000`–`3004` | 管理后台错误 |
|
||
| `3100` | 会话不存在 |
|
||
| `3201`–`3206` | 分片不存在、合并失败、分片过小/数量不符、会话过期、分片号无效 |
|
||
|
||
完整名称与中文说明以 `IM.Commons/ResultCode.cs` 为准。
|