feat(chat): 添加私信功能和WebSocket实时通信
- 集成WebSocket管理器,支持心跳、自动重连、消息队列 - 实现私信相关API接口,包括会话列表、历史消息、已读标记等 - 添加聊天页面和会话列表页面,支持实时消息收发 - 配置地图权限和腾讯地图SDK用于酒局定位功能 - 修改应用名称为"喝酒了么"并更新应用描述 - 在App.vue中初始化WebSocket连接和私信功能 - 添加详细的私信模块接口文档和Mock数据 - 优化单图片动态高度计算和预加载逻辑
This commit is contained in:
@@ -0,0 +1,465 @@
|
||||
# 干杯日记 - 私信模块接口文档
|
||||
|
||||
> 版本: 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 拉取)
|
||||
Reference in New Issue
Block a user