Files
wecom_it_smart_desk/backend/app/models/message.py
T

252 lines
9.1 KiB
Python
Raw Normal View History

# =============================================================================
# 企微IT智能服务台 — 消息模型
# =============================================================================
# 说明:对应数据库 messages 表,存储会话中的所有消息
# 消息来源:员工(employee)、坐席(agent)、AI(ai)、系统(system)
# 消息类型:文本(text)、图片(image)、文件(file)、语音(voice)、系统提示(system)
# =============================================================================
import uuid
from datetime import datetime, timedelta
from typing import Any, Dict, Optional
from sqlalchemy import Boolean, DateTime, ForeignKey, Index, Integer, JSON, String, Text
from sqlalchemy.orm import Mapped, mapped_column
from app.database import Base
class Message(Base):
"""消息模型 — 对应 messages 表。
每条消息都属于一个会话(Conversation),记录对话中的每一条信息。
包含发送者信息、消息内容、消息类型等。
Attributes:
id: 消息唯一标识(UUID,数据库自动生成)
conversation_id: 所属会话ID(外键,关联 conversations 表)
sender_type: 发送者类型(employee/agent/ai/system
sender_id: 发送者ID
sender_name: 发送者姓名(冗余存储,减少关联查询)
content: 消息内容(文本消息为文字,媒体消息为描述文字或URL)
msg_type: 消息类型(text/image/file/voice/system
media_id: 企微媒体文件ID(图片/语音/视频消息,3天有效)
media_url: 本地存储的媒体文件URL(下载后保存到服务器)
file_name: 文件名(文件消息用)
file_size: 文件大小(字节)
extra_data: 扩展元数据(JSON,如图片尺寸、语音格式等)
ai_suggestion: 是否为AI建议(坐席端展示用)
status: 消息状态(sending/sent/delivered/read
recallable_until: 可撤回截止时间(创建时间+2分钟)
is_read: 是否已读
created_at: 创建时间
"""
# 表名(必须和架构文档 DDL 一致)
__tablename__ = "messages"
# --------------------------------------------------------------------------
# 字段定义
# --------------------------------------------------------------------------
# 主键:UUIDPython端生成(兼容PostgreSQL和SQLite
id: Mapped[str] = mapped_column(
String(36),
primary_key=True,
default=lambda: str(uuid.uuid4()),
)
# 所属会话ID(外键,关联 conversations 表)
# ON DELETE CASCADE:删除会话时自动删除该会话的所有消息
conversation_id: Mapped[str] = mapped_column(
String(36),
ForeignKey("conversations.id", ondelete="CASCADE"),
nullable=False,
comment="所属会话ID",
)
# 发送者类型(CHECK 约束:只能取四种值)
# employee: 员工发送的消息
# agent: 坐席发送的消息
# ai: AI生成的消息(第二步启用)
# system: 系统消息(如"坐席已接入"等通知)
sender_type: Mapped[str] = mapped_column(
String(20),
nullable=False,
comment="发送者类型: employee/agent/ai/system",
)
# 发送者ID
# 员工消息时为企微UserID,坐席消息时为坐席user_id
sender_id: Mapped[str] = mapped_column(
String(64),
nullable=False,
comment="发送者ID",
)
# 发送者姓名(冗余存储,避免每次查消息都要关联用户表)
sender_name: Mapped[str] = mapped_column(
String(128),
nullable=False,
default="",
comment="发送者姓名",
)
# 消息内容
# 文本消息时为文本内容,图片/文件消息时为媒体URL或描述文字
content: Mapped[str] = mapped_column(
Text,
nullable=False,
default="",
comment="消息内容",
)
# 消息类型(CHECK 约束)
# text: 文本消息
# image: 图片消息
# file: 文件消息
# voice: 语音消息
# system: 系统消息
msg_type: Mapped[str] = mapped_column(
String(20),
nullable=False,
default="text",
comment="消息类型: text/image/file/voice/system",
)
# 引用回复:指向被回复的消息ID(M1 新增)
# 为 None 时表示普通消息,非 None 时表示对某条消息的回复
# 前端根据此字段显示引用内容(被回复消息的摘要)
reply_to_id: Mapped[Optional[str]] = mapped_column(
String(36),
nullable=True,
default=None,
comment="引用回复:被回复的消息ID",
)
# 企微媒体文件ID(图片/语音/视频消息携带)
# 注意:MediaId 仅3天有效,收到消息后应尽快下载保存到本地
# 下载接口:GET https://qyapi.weixin.qq.com/cgi-bin/media/get?access_token=TOKEN&media_id=MEDIA_ID
media_id: Mapped[Optional[str]] = mapped_column(
String(256),
nullable=True,
default=None,
comment="企微媒体文件ID3天有效)",
)
# 本地存储的媒体文件URL(下载后保存到服务器/NAS的访问路径)
# 格式示例:/media/2026/06/03/abc123.jpg
media_url: Mapped[Optional[str]] = mapped_column(
String(512),
nullable=True,
default=None,
comment="本地存储的媒体文件URL",
)
# 文件名(文件消息携带,或下载后自定义的文件名)
file_name: Mapped[Optional[str]] = mapped_column(
String(256),
nullable=True,
default=None,
comment="文件名",
)
# 文件大小(字节,文件消息携带)
file_size: Mapped[Optional[int]] = mapped_column(
Integer,
nullable=True,
default=None,
comment="文件大小(字节)",
)
# 扩展元数据(JSON格式,存储各消息类型的额外信息)
# 示例:
# 图片消息: {"pic_url": "https://...", "width": 1920, "height": 1080}
# 语音消息: {"format": "amr", "duration_seconds": 15}
# 视频消息: {"thumb_media_id": "...", "duration_seconds": 30}
# 位置消息: {"location_x": 23.134, "location_y": 113.358, "label": "杭州市"}
extra_data: Mapped[Optional[Dict[str, Any]]] = mapped_column(
JSON,
nullable=True,
default=None,
comment="扩展元数据(JSON",
)
# 是否为AI建议(坐席端展示用)
# True: 此消息为AI建议的回复,坐席可选择采纳/编辑/忽略
# False: 正常消息
ai_suggestion: Mapped[bool] = mapped_column(
Boolean,
nullable=False,
default=False,
comment="是否为AI建议",
)
# 消息状态(新增:sending/sent/delivered/read
# sending: 发送中
# sent: 已发送
# delivered: 已送达
# read: 已读
status: Mapped[str] = mapped_column(
String(20),
nullable=False,
default="sent",
comment="消息状态: sending/sent/delivered/read",
)
# 可撤回截止时间(创时间+2分钟)
# 用于判断消息是否在可撤回时间窗口内
recallable_until: Mapped[Optional[datetime]] = mapped_column(
DateTime(timezone=True),
nullable=True,
default=None,
comment="可撤回截止时间",
)
# 是否已读
# 用于统计未读消息数(坐席端显示红点)
is_read: Mapped[bool] = mapped_column(
Boolean,
nullable=False,
default=False,
comment="是否已读",
)
# 坐席对 AI 建议的操作行为
# 仅当 ai_suggestion=True 时有意义
# accepted: 坐席直接采纳了AI建议
# edited: 坐席编辑后采纳了AI建议
# ignored: 坐席忽略了AI建议
suggestion_action: Mapped[Optional[str]] = mapped_column(
String(20),
nullable=True,
default=None,
comment="AI建议操作行为: accepted/edited/ignored",
)
# 创建时间
created_at: Mapped[datetime] = mapped_column(
DateTime(timezone=True),
nullable=False,
default=datetime.now,
comment="创建时间",
)
# --------------------------------------------------------------------------
# 索引定义(和架构文档 DDL 严格一致)
# --------------------------------------------------------------------------
__table_args__ = (
# 按会话ID查询(如查询某会话的所有消息)
Index("idx_messages_conversation_id", "conversation_id"),
# 按创建时间查询(如按时间排序消息)
Index("idx_messages_created_at", "created_at"),
# 复合索引:按会话+时间查询(最常见的查询:获取某会话的消息列表)
Index("idx_messages_conversation_created", "conversation_id", "created_at"),
)
def __repr__(self) -> str:
"""消息对象的字符串表示,方便调试。"""
return (
f"<Message(id={self.id}, conv={self.conversation_id}, "
f"from={self.sender_type}, type={self.msg_type})>"
)