- 替换所有文件中的品牌名称引用,包括App.vue、pages.json、uni.scss等 - 更新全局样式、导航栏标题和组件中的文案显示 - 修改分享功能中的应用名称显示 - 调整聊天页面的输入栏键盘适配逻辑 - 在多个页面组件中添加事件发射声明以支持交互功能
466 lines
12 KiB
Markdown
466 lines
12 KiB
Markdown
# 碰盏日记 - 私信模块接口文档
|
||
|
||
> 版本: v1.0
|
||
> 日期: 2026-07-21
|
||
> 模块: 私信(实时通信)
|
||
> 小程序: 碰盏日记(uni-app 微信小程序)
|
||
> 基础URL: `https://dev.wash-painting.cn/api/v1`
|
||
> WebSocket: `wss://dev.wash-painting.cn/ws`
|
||
|
||
---
|
||
|
||
## 1. 模块概述
|
||
|
||
私信是酒友圈的实时通信子模块,允许酒友之间一对一发送消息。
|
||
|
||
| 子功能 | 说明 |
|
||
|--------|------|
|
||
| 会话列表 | 展示所有聊天会话,含最后消息、未读数 |
|
||
| 聊天记录 | 分页加载历史消息 |
|
||
| 实时收发 | 通过 WebSocket 实时推送/接收消息 |
|
||
| 已读回执 | 标记消息已读,通知对方 |
|
||
| 输入状态 | 实时显示"对方正在输入..." |
|
||
|
||
**前端当前状态:** 所有 REST 接口已在 `common/api.js` 中预留方法(暂用 Mock);WebSocket 管理器已在 `common/websocket.js` 中实现(含心跳、重连、消息队列)。后端实现后前端仅需切换调用方式。**请严格按照本文档的字段名和协议格式实现**。
|
||
|
||
---
|
||
|
||
## 2. 通用约定
|
||
|
||
### 2.1 鉴权
|
||
- REST 请求头:`Authorization: Bearer {token}`
|
||
- WebSocket 连接:`wss://dev.wash-painting.cn/ws?token={token}`
|
||
- 所有接口均需登录态
|
||
|
||
### 2.2 REST 统一响应格式
|
||
```json
|
||
{
|
||
"code": 0,
|
||
"message": "success",
|
||
"data": {}
|
||
}
|
||
```
|
||
|
||
### 2.3 错误码
|
||
| code | 含义 |
|
||
|------|------|
|
||
| 0 | 成功 |
|
||
| 400 | 参数错误 |
|
||
| 401 | 未登录/token过期 |
|
||
| 403 | 无权限(如:非酒友关系不可发私信) |
|
||
| 404 | 资源不存在 |
|
||
| 500 | 服务器错误 |
|
||
|
||
### 2.4 时间格式
|
||
- REST 接口时间字段使用 ISO 8601:`2026-07-21T22:30:00.000Z`
|
||
- WebSocket 消息中 timestamp 使用**毫秒时间戳**(如 `1721560000000`),便于前端排序
|
||
|
||
---
|
||
|
||
## 3. REST 接口
|
||
|
||
### 3.1 获取会话列表
|
||
```
|
||
GET /chat/conversations
|
||
```
|
||
|
||
**查询参数:** 无
|
||
|
||
**响应 data:**
|
||
```json
|
||
{
|
||
"list": [
|
||
{
|
||
"id": "conv_001",
|
||
"friendId": "user_002",
|
||
"nickname": "老张",
|
||
"avatar": "https://xxx/avatar.jpg",
|
||
"lastMessage": "今晚一起喝点?",
|
||
"lastTime": "2026-07-21T10:32:00.000Z",
|
||
"unread": 2,
|
||
"online": true
|
||
}
|
||
]
|
||
}
|
||
```
|
||
|
||
**Conversation 字段说明:**
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| id | string | 会话ID |
|
||
| friendId | string | 对方用户ID |
|
||
| nickname | string | 对方昵称 |
|
||
| avatar | string | 对方头像URL(可为空字符串) |
|
||
| lastMessage | string | 最后一条消息摘要(截取前30字) |
|
||
| lastTime | string | 最后消息时间 ISO 8601 |
|
||
| unread | int | 未读消息数 |
|
||
| online | boolean | 对方是否在线 |
|
||
|
||
**业务规则:**
|
||
- 按 lastTime 倒序排列
|
||
- 仅返回有消息记录的会话
|
||
- 仅酒友之间可存在会话
|
||
|
||
---
|
||
|
||
### 3.2 获取历史消息
|
||
```
|
||
GET /chat/messages
|
||
```
|
||
|
||
**查询参数:**
|
||
| 参数 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| conversationId | string | 是 | 会话ID |
|
||
| page | int | 否 | 页码,默认1 |
|
||
| pageSize | int | 否 | 每页数量,默认20 |
|
||
|
||
**响应 data:**
|
||
```json
|
||
{
|
||
"list": [
|
||
{
|
||
"id": "msg_001",
|
||
"conversationId": "conv_001",
|
||
"senderId": "user_002",
|
||
"receiverId": "user_001",
|
||
"type": "text",
|
||
"content": "今晚一起喝点?",
|
||
"timestamp": 1721556300000,
|
||
"status": "read"
|
||
}
|
||
],
|
||
"hasMore": true
|
||
}
|
||
```
|
||
|
||
**Message 字段说明:**
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| id | string | 消息ID(服务端生成) |
|
||
| conversationId | string | 所属会话ID |
|
||
| senderId | string | 发送者用户ID |
|
||
| receiverId | string | 接收者用户ID |
|
||
| type | string | 消息类型:`text` / `image` |
|
||
| content | string | 消息内容(文本内容 或 图片URL) |
|
||
| timestamp | long | 发送时间(毫秒时间戳) |
|
||
| status | string | 消息状态:`sent` / `delivered` / `read` |
|
||
|
||
**业务规则:**
|
||
- 按 timestamp 正序返回(最早在前)
|
||
- status 表示该消息对接收方的状态(发送方自己的消息显示对方是否已读)
|
||
|
||
---
|
||
|
||
### 3.3 标记会话已读
|
||
```
|
||
POST /chat/conversations/:id/read
|
||
```
|
||
|
||
**请求参数:** 无(路径参数 conversationId)
|
||
|
||
**响应 data:**
|
||
```json
|
||
{ "success": true }
|
||
```
|
||
|
||
**业务规则:**
|
||
- 将该会话中所有对方发来的消息标记为 read
|
||
- 会话的 unread 清零
|
||
- 幂等处理
|
||
|
||
---
|
||
|
||
### 3.4 获取未读消息总数
|
||
```
|
||
GET /chat/unread-count
|
||
```
|
||
|
||
**响应 data:**
|
||
```json
|
||
{ "total": 3 }
|
||
```
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| total | int | 所有会话未读消息总和 |
|
||
|
||
---
|
||
|
||
## 4. WebSocket 协议
|
||
|
||
### 4.1 连接
|
||
|
||
```
|
||
wss://dev.wash-painting.cn/ws?token={jwt_token}
|
||
```
|
||
|
||
- 连接建立后服务端验证 token,无效则返回 close code 4001
|
||
- 连接成功后服务端推送:
|
||
```json
|
||
{ "type": "connected", "data": { "userId": "user_001" } }
|
||
```
|
||
|
||
### 4.2 消息帧格式
|
||
|
||
所有 WebSocket 消息均为 JSON 文本帧:
|
||
|
||
```json
|
||
{
|
||
"type": "消息类型",
|
||
"data": { ... },
|
||
"ts": 1721560000000
|
||
}
|
||
```
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| type | string | 消息类型标识 |
|
||
| data | object | 业务数据(type=ping/pong 时可省略) |
|
||
| ts | long | 客户端发送时间戳(毫秒),服务端原样返回用于延迟计算 |
|
||
|
||
---
|
||
|
||
### 4.3 客户端 → 服务端
|
||
|
||
#### 发送聊天消息
|
||
```json
|
||
{
|
||
"type": "chat",
|
||
"data": {
|
||
"receiverId": "user_002",
|
||
"content": "今晚一起喝点?",
|
||
"msgType": "text",
|
||
"clientMsgId": "msg_1721560000000_a3f2"
|
||
},
|
||
"ts": 1721560000000
|
||
}
|
||
```
|
||
|
||
| 字段 | 类型 | 必填 | 说明 |
|
||
|------|------|------|------|
|
||
| receiverId | string | 是 | 接收者用户ID |
|
||
| content | string | 是 | 消息内容(文本 或 图片URL) |
|
||
| msgType | string | 是 | `text` / `image` |
|
||
| clientMsgId | string | 是 | 客户端生成的临时消息ID(用于ACK匹配) |
|
||
|
||
#### 发送输入状态
|
||
```json
|
||
{
|
||
"type": "typing",
|
||
"data": { "receiverId": "user_002" },
|
||
"ts": 1721560000000
|
||
}
|
||
```
|
||
|
||
#### 发送已读回执
|
||
```json
|
||
{
|
||
"type": "read",
|
||
"data": {
|
||
"conversationId": "conv_001",
|
||
"lastMsgId": "msg_006"
|
||
},
|
||
"ts": 1721560000000
|
||
}
|
||
```
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| conversationId | string | 会话ID |
|
||
| lastMsgId | string | 已读到的最后一条消息ID |
|
||
|
||
#### 心跳
|
||
```json
|
||
{ "type": "ping" }
|
||
```
|
||
|
||
---
|
||
|
||
### 4.4 服务端 → 客户端
|
||
|
||
#### 推送新消息
|
||
```json
|
||
{
|
||
"type": "chat",
|
||
"data": {
|
||
"msgId": "msg_srv_001",
|
||
"senderId": "user_002",
|
||
"receiverId": "user_001",
|
||
"conversationId": "conv_001",
|
||
"content": "好啊,几点?",
|
||
"msgType": "text",
|
||
"timestamp": 1721560060000
|
||
}
|
||
}
|
||
```
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| msgId | string | 服务端生成的消息ID |
|
||
| senderId | string | 发送者ID |
|
||
| receiverId | string | 接收者ID |
|
||
| conversationId | string | 会话ID |
|
||
| content | string | 消息内容 |
|
||
| msgType | string | `text` / `image` |
|
||
| timestamp | long | 服务端接收时间(毫秒时间戳) |
|
||
|
||
#### 消息送达确认(ACK)
|
||
```json
|
||
{
|
||
"type": "ack",
|
||
"data": {
|
||
"clientMsgId": "msg_1721560000000_a3f2",
|
||
"msgId": "msg_srv_001",
|
||
"status": "sent"
|
||
}
|
||
}
|
||
```
|
||
|
||
| 字段 | 类型 | 说明 |
|
||
|------|------|------|
|
||
| clientMsgId | string | 客户端原始消息ID(用于匹配) |
|
||
| msgId | string | 服务端分配的正式消息ID |
|
||
| status | string | `sent`(已入库) |
|
||
|
||
> 前端收到 ACK 后将本地消息的 id 替换为 msgId,status 更新为 sent。
|
||
|
||
#### 推送输入状态
|
||
```json
|
||
{
|
||
"type": "typing",
|
||
"data": { "senderId": "user_002" }
|
||
}
|
||
```
|
||
|
||
#### 推送已读回执
|
||
```json
|
||
{
|
||
"type": "read",
|
||
"data": {
|
||
"conversationId": "conv_001",
|
||
"readerId": "user_002"
|
||
}
|
||
}
|
||
```
|
||
|
||
> 前端收到后将该会话中自己发送的消息 status 更新为 read。
|
||
|
||
#### 心跳响应
|
||
```json
|
||
{ "type": "pong" }
|
||
```
|
||
|
||
---
|
||
|
||
### 4.5 心跳与重连机制(前端已实现,后端需配合)
|
||
|
||
| 机制 | 说明 |
|
||
|------|------|
|
||
| 心跳间隔 | 前端每 **30秒** 发送 `ping`,后端收到后立即回复 `pong` |
|
||
| 超时判定 | 前端 60秒 未收到 `pong` 则主动断开重连 |
|
||
| 重连策略 | 指数退避:1s → 2s → 4s → 8s → ... → 最大30s |
|
||
| 离线消息 | 用户重新连接后,后端应推送离线期间收到的消息(按时间正序) |
|
||
| 连接互踢 | 同一用户多端登录时,旧连接推送 `{"type":"kicked"}` 后关闭 |
|
||
|
||
---
|
||
|
||
## 5. 数据库表设计参考
|
||
|
||
### 5.1 会话表 (conversations)
|
||
```sql
|
||
CREATE TABLE conversations (
|
||
id VARCHAR(32) PRIMARY KEY,
|
||
user_a_id VARCHAR(32) NOT NULL COMMENT '用户A(ID较小者)',
|
||
user_b_id VARCHAR(32) NOT NULL COMMENT '用户B(ID较大者)',
|
||
last_message VARCHAR(128) COMMENT '最后消息摘要',
|
||
last_message_time DATETIME COMMENT '最后消息时间',
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
|
||
UNIQUE INDEX idx_users (user_a_id, user_b_id),
|
||
INDEX idx_user_a (user_a_id, updated_at),
|
||
INDEX idx_user_b (user_b_id, updated_at)
|
||
);
|
||
```
|
||
> 两人之间只有一个会话,user_a_id < user_b_id 保证唯一。
|
||
|
||
### 5.2 消息表 (messages)
|
||
```sql
|
||
CREATE TABLE messages (
|
||
id VARCHAR(32) PRIMARY KEY,
|
||
conversation_id VARCHAR(32) NOT NULL,
|
||
sender_id VARCHAR(32) NOT NULL,
|
||
receiver_id VARCHAR(32) NOT NULL,
|
||
type ENUM('text', 'image') DEFAULT 'text',
|
||
content TEXT NOT NULL,
|
||
status ENUM('sent', 'delivered', 'read') DEFAULT 'sent',
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
INDEX idx_conv_time (conversation_id, created_at),
|
||
INDEX idx_receiver_status (receiver_id, status)
|
||
);
|
||
```
|
||
|
||
### 5.3 未读计数表 (unread_counts)
|
||
```sql
|
||
CREATE TABLE unread_counts (
|
||
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
||
user_id VARCHAR(32) NOT NULL,
|
||
conversation_id VARCHAR(32) NOT NULL,
|
||
count INT DEFAULT 0,
|
||
UNIQUE INDEX idx_user_conv (user_id, conversation_id),
|
||
INDEX idx_user (user_id)
|
||
);
|
||
```
|
||
> 收到新消息时 count+1,标记已读时归零。
|
||
|
||
---
|
||
|
||
## 6. 业务规则汇总
|
||
|
||
| 规则 | 说明 |
|
||
|------|------|
|
||
| 酒友限制 | 仅酒友关系可发私信,非酒友返回 403 |
|
||
| 消息长度 | 文本消息最长 500 字 |
|
||
| 图片消息 | content 为图片URL(先调上传接口),前端展示为图片气泡 |
|
||
| 内容安全 | 文本消息需接入微信 msgSecCheck 审核,图片需接入 imgSecCheck |
|
||
| 会话自动创建 | 首次发消息时自动创建会话记录 |
|
||
| 未读计数 | 新消息到达时接收方 unread+1;调用标记已读或 WebSocket read 事件时归零 |
|
||
| 消息撤回 | v1.0 暂不支持,后续扩展 |
|
||
| 离线推送 | v1.0 暂不接入微信订阅消息推送,用户上线后拉取未读即可 |
|
||
|
||
---
|
||
|
||
## 7. 前端对接说明
|
||
|
||
### 7.1 前端调用位置
|
||
|
||
**REST 接口**(`common/api.js`):
|
||
```js
|
||
GetConversations() // GET /chat/conversations
|
||
GetMessages({ conversationId, page }) // GET /chat/messages
|
||
MarkRead({ conversationId }) // POST /chat/conversations/:id/read
|
||
GetUnreadCount() // GET /chat/unread-count
|
||
```
|
||
|
||
**WebSocket**(`common/websocket.js`):
|
||
```js
|
||
wsManager.connect(token) // 建立连接
|
||
wsManager.send('chat', { receiverId, content, msgType, clientMsgId })
|
||
wsManager.send('typing', { receiverId })
|
||
wsManager.send('read', { conversationId, lastMsgId })
|
||
wsManager.on('message', handler) // 收到新消息
|
||
wsManager.on('typing', handler) // 对方正在输入
|
||
wsManager.on('read', handler) // 对方已读
|
||
wsManager.on('ack', handler) // 消息送达确认
|
||
```
|
||
|
||
### 7.2 注意事项
|
||
1. **字段名严格一致**:前端已按本文档字段名渲染,请勿更改命名
|
||
2. **timestamp 为毫秒时间戳**:WebSocket 消息中的时间使用 long 型毫秒时间戳
|
||
3. **clientMsgId 必须原样返回**:ACK 中需包含客户端发送的 clientMsgId,前端依赖此字段匹配本地消息
|
||
4. **conversationId 生成规则**:建议用双方 userId 排序拼接(如 `conv_{minId}_{maxId}`),确保两人间唯一
|
||
5. **头像可为空**:avatar 字段允许空字符串,前端有默认占位头像
|
||
6. **内容安全**:消息内容需接入微信内容安全审核(msgSecCheck / imgSecCheck)
|
||
7. **幂等性**:标记已读接口需幂等,重复调用不报错
|
||
8. **离线消息**:用户重连后需推送离线消息,或前端通过 REST 接口拉取(当前方案为 REST 拉取)
|