# ============================================================================= # 企微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" # -------------------------------------------------------------------------- # 字段定义 # -------------------------------------------------------------------------- # 主键:UUID,Python端生成(兼容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="企微媒体文件ID(3天有效)", ) # 本地存储的媒体文件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"" )