# 消息功能详细方案 > **版本**: 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 ``` --- ## 七、前端交互设计 ### 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 列表(默认支持) ``` 常用: 👍 👎 😊 😂 😢 😡 ❤️ 🔥 👏 🎉 😎 ```