Files
hejiu/docs/私信模块接口文档.md
T
cg 99723497fa rename(app): 将应用名称从"干杯日记"更改为"碰盏日记"
- 替换所有文件中的品牌名称引用,包括App.vue、pages.json、uni.scss等
- 更新全局样式、导航栏标题和组件中的文案显示
- 修改分享功能中的应用名称显示
- 调整聊天页面的输入栏键盘适配逻辑
- 在多个页面组件中添加事件发射声明以支持交互功能
2026-07-21 22:20:08 +08:00

466 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 碰盏日记 - 私信模块接口文档
> 版本: 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 替换为 msgIdstatus 更新为 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 '用户AID较小者)',
user_b_id VARCHAR(32) NOT NULL COMMENT '用户BID较大者)',
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 拉取)