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

252 lines
9.1 KiB
Python
Raw 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.
# =============================================================================
# 企微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})>"
)