Files
wecom_it_smart_desk/docs/消息功能详细方案.md

535 lines
15 KiB
Markdown
Raw Permalink 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-06-14
> **优先级**: P0 - 最高优先级
---
## 一、现状与目标
### 1.1 当前问题
| 问题 | 影响 | 优先级 |
|------|------|--------|
| 3秒轮询,实时性差 | 用户体验差 | P0 |
| 无消息状态 | 不知道是否送达 | P0 |
| 无表情回应 | 交互单调 | P1 |
| 无截图功能 | 无法快速上报问题 | P1 |
| 媒体处理耦合企微 | 3天失效风险 | P0 |
### 1.2 目标
```
┌─────────────────────────────────────────────────────────┐
│ 消息功能 V2 目标 │
├─────────────────────────────────────────────────────────┤
│ ✅ 实时性: WebSocket 推送,毫秒级响应 │
│ ✅ 消息状态: sent→delivered→read │
│ ✅ 表情回应: emoji reactions │
│ ✅ 截图上传: 屏幕截图快速上报 │
│ ✅ 媒体独立: 本地存储,解耦企微 │
│ ✅ 已读回执: 双向可见 │
└─────────────────────────────────────────────────────────┘
```
---
## 二、架构设计
### 2.1 技术选型
| 组件 | 选型 | 说明 |
|------|------|------|
| 实时通信 | WebSocket | 已有基础(ws_manager |
| 消息状态 | Redis Key-Event | 轻量实现 |
| 媒体存储 | 本地文件系统 + NAS | 解耦企微 |
| 截图工具 | html2canvas + 粘贴 | 浏览器原生 |
### 2.2 系统架构
```
┌─────────────────┐
│ WebSocket │
│ 实时推送 │
└───────┬─────────┘
┌───────────────────┼───────────────────┐
▼ ▼ ▼
┌─────────┐ ┌─────────┐ ┌─────────┐
│H5用户端 │ │坐席工作台│ │管理后台 │
└────┬────┘ └────┬────┘ └────┬────┘
│ │ │
└───────────────────┼───────────────────┘
┌───────────────────────┐
│ 后端 WebSocket │
│ ws_manager │
└───────────┬───────────┘
┌────────────────┼────────────────┐
▼ ▼ ▼
┌──────────┐ ┌──────────┐ ┌──────────┐
│消息状态 │ │媒体存储 │ │事件广播 │
│Redis │ │本地/NAS │ │Channel │
└──────────┘ └─────────┘ └──────────┘
```
### 2.3 数据流
```
用户A发送消息
POST /messages (创建消息,status=sent)
WebSocket 广播 new_message 给用户B
├── 用户B收到 → status=delivered
├── 用户B读取 → status=read + 已读回执
└── 用户A收到回执 → 更新消息状态
```
---
## 三、消息模型扩展
### 3.1 新增字段
```python
# backend/app/models/message.py 新增
class Message(Base):
# ... 现有字段 ...
# --------------------------------------------------------------------------
# V2 新增字段
# --------------------------------------------------------------------------
# 消息状态(V2新增)
# sent: 已发送
# delivered: 已送达(对方收到)
# read: 已读
message_status: Mapped[str] = mapped_column(
String(20),
nullable=False,
default="sent",
comment="消息状态: sent/delivered/read",
)
# 表情回应(V2新增)
# 存储格式: {"👍": "user_id", "👎": "user_id", "😊": "user_id"}
# 每个用户只能对同一消息添加一个表情
reactions: Mapped[Optional[Dict[str, str]]] = mapped_column(
JSON,
nullable=True,
default=None,
comment="表情回应: {emoji: user_id}",
)
# 消息来源设备(V2新增)
# mobile: 手机端发送
# desktop: 桌面端发送
device_type: Mapped[str] = mapped_column(
String(20),
nullable=False,
default="desktop",
comment="设备类型: mobile/desktop",
)
# 已读用户列表(V2新增)
# 存储已读该消息的用户ID列表
read_by: Mapped[Optional[List[str]]] = mapped_column(
JSON,
nullable=True,
default=None,
comment="已读用户列表",
)
```
### 3.2 DDL
```sql
-- 消息模型 V2 DDL
ALTER TABLE messages
ADD COLUMN message_status VARCHAR(20) NOT NULL DEFAULT 'sent',
ADD COLUMN reactions JSON,
ADD COLUMN device_type VARCHAR(20) NOT NULL DEFAULT 'desktop',
ADD COLUMN read_by JSON;
-- 新增索引
CREATE INDEX idx_messages_status ON messages(message_status);
CREATE INDEX idx_messages_conversation_status ON messages(conversation_id, message_status);
```
---
## 四、API 设计
### 4.1 现有 API(保持兼容)
| 端点 | 方法 | 状态 |
|------|------|------|
| `/messages` | GET | ✅ 兼容 |
| `/messages` | POST | ✅ 兼容 |
### 4.2 新增 API
| 端点 | 方法 | 功能 |
|------|------|------|
| `/messages/{id}/status` | PATCH | 更新消息状态 |
| `/messages/{id}/reactions` | POST | 添加表情回应 |
| `/messages/{id}/reactions` | DELETE | 移除表情回应 |
| `/messages/poll` | GET | 轮询(保留兼容) |
### 4.3 API 详情
#### 4.3.1 更新消息状态
```http
PATCH /api/messages/{id}/status
Content-Type: application/json
Request:
{
"status": "delivered" | "read"
}
Response:
{
"id": "uuid",
"message_status": "delivered" | "read",
"read_by": ["user_id_1", "user_id_2"]
}
```
#### 4.3.2 添加表情回应
```http
POST /api/messages/{id}/reactions
Content-Type: application/json
Request:
{
"emoji": "👍" // emoji Unicode
}
Response:
{
"id": "uuid",
"reactions": {
"👍": "user_id_1",
"😊": "user_id_2"
}
}
```
#### 4.3.3 移除表情回应
```http
DELETE /api/messages/{id}/reactions
Response:
{
"id": "uuid",
"reactions": {
"😊": "user_id_2"
}
}
```
---
## 五、WebSocket 事件
### 5.1 现有事件(保持)
| 事件名 | 方向 | 说明 |
|--------|------|------|
| `new_message` | Server→Client | 新消息 |
| `conversation_updated` | Server→Client | 会话更新 |
### 5.2 新增事件
| 事件名 | 方向 | 说明 |
|--------|------|------|
| `message_status_changed` | Server→Client | 消息状态变更 |
| `reaction_added` | Server→Client | 表情回应添加 |
| `reaction_removed` | Server→Client | 表情回应移除 |
| `typing` | Client→Server | 对方正在输入 |
| `typing` | Server→Client | 对方正在输入通知 |
### 5.3 事件格式
#### 5.3.1 消息状态变更
```json
{
"event": "message_status_changed",
"data": {
"message_id": "uuid",
"status": "delivered",
"changed_by": "user_id",
"timestamp": "2026-06-14T11:30:00Z"
}
}
```
#### 5.3.2 表情回应
```json
{
"event": "reaction_added",
"data": {
"message_id": "uuid",
"emoji": "👍",
"user_id": "user_id",
"user_name": "张三"
}
}
```
#### 5.3.3 Typing 通知
```json
{
"event": "typing",
"data": {
"conversation_id": "uuid",
"user_id": "user_id",
"user_name": "张三",
"is_typing": true
}
}
```
---
## 六、媒体处理(截图/图片/文件)
### 6.1 架构
```
┌─────────────────────────────────────────────────────────────┐
│ 媒体处理架构 │
├─────────────────────────────────────────────────────────────┤
│ │
│ 用户上传 ──▶ 前端压缩/裁剪 ──▶ 上传API ──▶ 本地存储 │
│ │ │ │
│ ▼ ▼ │
│ 生成缩略图 返回 media_url │
│ │ │ │
│ ▼ ▼ │
│ 消息内容引用 media_url │
│ │
└─────────────────────────────────────────────────────────────┘
```
### 6.2 上传流程
```python
# backend/app/api/upload.py
@router.post("/upload", dependencies=[require_auth])
async def upload_media(
file: UploadFile,
file_type: str = Form(...), # image/file/screenshot
):
"""媒体文件上传"""
# 1. 验证文件类型
allowed_types = {
"image": ["image/jpeg", "image/png", "image/gif", "image/webp"],
"file": ["application/pdf", "application/msword",
"application/vnd.openxmlformats-officedocument.wordprocessingml.document"],
"screenshot": ["image/png", "image/webp"],
}
if file.content_type not in allowed_types.get(file_type, []):
raise HTTPException(400, "不支持的文件类型")
# 2. 验证文件大小(10MB
if file.size > 10 * 1024 * 1024:
raise HTTPException(400, "文件大小不能超过10MB")
# 3. 生成存储路径
date_str = datetime.now().strftime("%Y/%m/%d")
file_ext = Path(file.filename).suffix
unique_name = f"{uuid.uuid4()}{file_ext}"
relative_path = f"/media/{date_str}/{unique_name}"
# 4. 保存到本地
upload_dir = Path(settings.MEDIA_UPLOAD_DIR) / date_str
upload_dir.mkdir(parents=True, exist_ok=True)
file_path = upload_dir / unique_name
content = await file.read()
file_path.write_bytes(content)
# 5. 生成缩略图(图片)
thumbnail_url = None
if file_type == "image" or file_type == "screenshot":
thumbnail_url = await generate_thumbnail(file_path, unique_name)
return {
"media_url": relative_path,
"thumbnail_url": thumbnail_url,
"file_size": len(content),
"file_name": file.filename,
}
```
### 6.3 截图功能
```javascript
// frontend-h5/src/components/ChatInput.vue
<script setup>
import { ref } from 'vue'
const handlePaste = async (event) => {
const items = event.clipboardData?.items
if (!items) return
for (const item of items) {
if (item.type.startsWith('image/')) {
const blob = item.getAsFile()
if (blob) {
await uploadScreenshot(blob)
}
}
}
}
const uploadScreenshot = async (blob) => {
const formData = new FormData()
formData.append('file', blob, 'screenshot.png')
formData.append('file_type', 'screenshot')
const response = await fetch('/api/upload', {
method: 'POST',
body: formData,
})
const data = await response.json()
emit('image-uploaded', data.media_url)
}
</script>
<template>
<div @paste="handlePaste">
<!-- 输入框区域 -->
</div>
</template>
```
---
## 七、前端交互设计
### 7.1 消息卡片(V2
```
┌────────────────────────────────────────────────────┐
│ 👤 张三 11:30 ✓✓ 已读 │
│ │
│ 这是消息内容... │
│ │
│ ┌─────┐ │
│ │图片 │ ← 点击可预览 │
│ └─────┘ │
│ │
│ 👍👎😊 ← 表情回应(点击选择) │
└────────────────────────────────────────────────────┘
```
### 7.2 表情选择器
```
┌──────────────────────────┐
│ 👍 👎 😊 😂 😢 😡 ❤️ 🔥 │
│ │
│ [自定义表情...] │
└──────────────────────────┘
```
### 7.3 截图快捷键
| 平台 | 快捷键 |
|------|--------|
| Windows | `Win + Shift + S` / `Ctrl + V` 粘贴 |
| macOS | `Cmd + Shift + 4` / `Cmd + V` 粘贴 |
---
## 八、实施计划
### 8.1 任务拆分
| 序号 | 任务 | 工作量 | 依赖 |
|------|------|--------|------|
| T1 | 消息模型扩展 | 1d | - |
| T2 | 媒体上传API | 2d | T1 |
| T3 | WebSocket事件 | 1d | - |
| T4 | 消息状态API | 1d | T1 |
| T5 | 表情回应API | 1d | T1 |
| T6 | 坐席端V2 | 2d | T3,T4,T5 |
| T7 | H5端V2 | 2d | T2,T3,T4,T5 |
| T8 | 截图功能 | 1d | T2 |
| T9 | 联调测试 | 2d | T6,T7,T8 |
### 8.2 时间估算
```
总工期: 12 工作日
Week 1: ████████░░░░░░░░░░
模型+API+WS (5d)
Week 2: ░░░░░░░████████░░░
前端+截图 (5d)
Week 3: ░░░░░░░░░░░░████
联调测试 (2d)
```
---
## 九、兼容性
### 9.1 向后兼容
| 场景 | 处理 |
|------|------|
| 旧客户端连接 | 消息状态字段有默认值,不影响 |
| 轮询仍然工作 | 保留 `/messages/poll` 兼容 |
| 媒体未迁移 | 企微MediaID仍然可用 |
### 9.2 降级策略
| 故障场景 | 降级方案 |
|----------|----------|
| WS连接失败 | 降级到轮询 |
| 媒体上传失败 | 提示用户重试 |
| 表情功能不可用 | 隐藏表情按钮 |
---
## 十、待确认事项
- [ ] 媒体存储路径(本地 vs NAS
- [ ] 文件大小限制(当前10MB
- [ ] 支持的截图快捷键
- [ ] 表情包自定义权限
---
## 附录
### A. Emoji 列表(默认支持)
```
常用: 👍 👎 😊 😂 😢 😡 ❤️ 🔥 👏 🎉 😎
```