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

15 KiB
Raw Permalink Blame History

消息功能详细方案

版本: 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 新增字段

# 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

-- 消息模型 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 更新消息状态

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 添加表情回应

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 移除表情回应

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 消息状态变更

{
    "event": "message_status_changed",
    "data": {
        "message_id": "uuid",
        "status": "delivered",
        "changed_by": "user_id",
        "timestamp": "2026-06-14T11:30:00Z"
    }
}

5.3.2 表情回应

{
    "event": "reaction_added",
    "data": {
        "message_id": "uuid",
        "emoji": "👍",
        "user_id": "user_id",
        "user_name": "张三"
    }
}

5.3.3 Typing 通知

{
    "event": "typing",
    "data": {
        "conversation_id": "uuid",
        "user_id": "user_id",
        "user_name": "张三",
        "is_typing": true
    }
}

六、媒体处理(截图/图片/文件)

6.1 架构

┌─────────────────────────────────────────────────────────────┐
│                    媒体处理架构                          │
├─────────────────────────────────────────────────────────────┤
│                                                         │
│   用户上传 ──▶ 前端压缩/裁剪 ──▶ 上传API ──▶ 本地存储    │
│                     │                    │               │
│                     ▼                    ▼               │
│               生成缩略图          返回 media_url          │
│                     │                    │               │
│                     ▼                    ▼               │
│               消息内容引用 media_url                    │
│                                                         │
└─────────────────────────────────────────────────────────────┘

6.2 上传流程

# 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 截图功能

// 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 列表(默认支持)

常用: 👍 👎 😊 😂 😢 😡 ❤️ 🔥 👏 🎉 😎