# ============================================================================= # 企微IT智能服务台 — 会话模型 # ============================================================================= # 说明:对应数据库 conversations 表,存储所有会话信息 # 核心概念:每个员工的每次咨询对应一个会话(Conversation) # 会话状态流转:ai_handling → queued → serving → resolved # ============================================================================= import uuid from datetime import datetime from typing import Any, Dict, Optional from sqlalchemy import Boolean, DateTime, Index, Integer, JSON, String from sqlalchemy.orm import Mapped, mapped_column from app.database import Base class Conversation(Base): """会话模型 — 对应 conversations 表。 每个员工的一次完整咨询过程对应一个会话记录。 包含员工信息、会话状态、紧急度评分、标签等核心数据。 Attributes: id: 会话唯一标识(UUID,数据库自动生成) employee_id: 企微员工UserID(关联企微通讯录) employee_name: 员工姓名(冗余存储,减少关联查询) department: 员工部门 position: 员工岗位 level: 员工等级(用于 VIP 判断) status: 会话状态(ai_handling/queued/serving/resolved) is_vip: VIP标记(基于企微通讯录规则自动匹配) is_pinned: 置顶标记(坐席手动操作) is_todo: 代办标记(坐席手动操作) urgency_score: 紧急度评分(1-5,数值越大越紧急) tags: 标签集合(JSONB,存储举手/需介入/情绪等标记) assigned_agent_id: 分配的坐席ID last_message_at: 最后消息时间(用于会话排序) last_message_summary: 最后消息摘要(会话列表预览用) created_at: 创建时间 updated_at: 更新时间 """ # 表名(必须和架构文档 DDL 一致) __tablename__ = "conversations" # -------------------------------------------------------------------------- # 字段定义 # -------------------------------------------------------------------------- # 主键:UUID,Python端生成(兼容PostgreSQL和SQLite) id: Mapped[str] = mapped_column( String(36), primary_key=True, default=lambda: str(uuid.uuid4()), ) # 企业微信企业ID(US-7: 区分主企业和下游企业员工) # 默认值为主企业 corp_id,下游企业员工使用下游企业 corp_id corp_id: Mapped[str] = mapped_column( String(64), nullable=False, default="", comment="企业微信企业ID(主企业或下游企业)", ) # 企微员工UserID(NOT NULL,配合 corp_id 唯一标识员工) employee_id: Mapped[str] = mapped_column( String(64), nullable=False, comment="企微员工UserID", ) # 员工姓名(冗余存储,避免每次查询都要关联企微API) employee_name: Mapped[str] = mapped_column( String(128), nullable=False, default="", comment="员工姓名", ) # 部门 department: Mapped[str] = mapped_column( String(256), nullable=False, default="", comment="部门", ) # 岗位 position: Mapped[str] = mapped_column( String(128), nullable=False, default="", comment="岗位", ) # 等级(用于 VIP 判断:总监及以上为 VIP) level: Mapped[str] = mapped_column( String(64), nullable=False, default="", comment="等级", ) # 会话状态(CHECK 约束:只能取四种值) # ai_handling: AI处理中(第二步启用) # queued: 排队中,等待坐席接入 # serving: 服务中,坐席正在处理 # resolved: 已结单 status: Mapped[str] = mapped_column( String(20), nullable=False, default="queued", comment="会话状态: ai_handling/queued/serving/resolved", ) # VIP标记(基于企微通讯录API规则自动匹配) is_vip: Mapped[bool] = mapped_column( Boolean, nullable=False, default=False, comment="VIP标记", ) # 置顶标记(坐席手动操作,置顶的会话在列表中优先显示) is_pinned: Mapped[bool] = mapped_column( Boolean, nullable=False, default=False, comment="置顶标记", ) # 代办标记(坐席手动操作,标记需要后续跟进的会话) is_todo: Mapped[bool] = mapped_column( Boolean, nullable=False, default=False, comment="代办标记", ) # 紧急度评分(1-5,数值越大越紧急) # 计算公式:基础分 + 情绪加成 + VIP加成 + 重复追问加成 urgency_score: Mapped[int] = mapped_column( Integer, nullable=False, default=1, comment="紧急度1-5", ) # 标签集合(JSON 格式,存储结构化标记数据,兼容所有数据库) # 示例:{"hand_raise": true, "emotion": "angry", "need_intervene": true} tags: Mapped[Dict[str, Any]] = mapped_column( JSON, nullable=False, default=dict, comment="标签集合", ) # 分配的坐席ID(可为空,表示尚未分配坐席) assigned_agent_id: Mapped[Optional[str]] = mapped_column( String(64), nullable=True, comment="分配的坐席ID", ) # 协作坐席ID列表(JSON 数组,存储所有被邀请来协作的坐席ID) # 和 assigned_agent_id 的区别: # - assigned_agent_id:会话的「主责」坐席(接单人),只有他才能结单/转接 # - collaborating_agent_ids:被邀请来协助的坐席,可以查看和回复,但不能结单 # 设计决策:用 JSON 而非关联表,因为协作人数少(1-3人),JSON 查询足够 collaborating_agent_ids: Mapped[list] = mapped_column( JSON, nullable=False, default=list, comment="协作坐席ID列表", ) # 被邀请参与会话的非坐席人员列表(JSON 数组,存储员工ID或部门ID) # 和 collaborating_agent_ids 的区别: # - collaborating_agent_ids:坐席 → 坐席协作(摇人) # - participants:坐席 → 任意员工/部门(邀请功能 P0-09~P0-11) # 格式:[ {"id": "employee_id", "name": "姓名", "department": "部门", "type": "employee"}, # {"id": "dept_id", "name": "部门名", "type": "department"} ] # 设计决策:存储完整信息,减少企微API调用 participants: Mapped[list] = mapped_column( JSON, nullable=False, default=list, comment="被邀请参与会话的人员列表(邀请功能)", ) # AI 实质性回复计数(排除打招呼/呼叫人工的引导回复) # 当计数 >= 3 时,前端显示「呼叫坐席」按钮 ai_substantive_reply_count: Mapped[int] = mapped_column( Integer, nullable=False, default=0, comment="AI实质性回复计数(满3次可呼叫坐席)", ) # 影响范围(受影响人数,0=未评估,数值越大影响范围越广) impact_scope: Mapped[int] = mapped_column( Integer, nullable=False, default=0, comment="影响范围", ) # 阻断性标记(问题是否阻断员工正常工作流程) is_blocking: Mapped[bool] = mapped_column( Boolean, nullable=False, default=False, comment="阻断性标记", ) # 情绪状态(normal: 正常, worried: 担忧, angry: 愤怒, urgent: 紧急) emotion_state: Mapped[str] = mapped_column( String(20), nullable=False, default="normal", comment="情绪状态", ) # Dify 会话ID(用于多轮对话上下文保持) # Dify 侧通过此 ID 关联同一员工的多轮对话 dify_conversation_id: Mapped[Optional[str]] = mapped_column( String(128), nullable=True, default=None, comment="Dify会话ID(多轮对话上下文)", ) # 最后消息时间(用于会话列表按最新消息排序) last_message_at: Mapped[Optional[datetime]] = mapped_column( DateTime(timezone=True), nullable=True, comment="最后消息时间", ) # 最后消息摘要(会话列表预览用,截取消息前256字符) last_message_summary: Mapped[str] = mapped_column( String(256), nullable=False, default="", comment="最后消息摘要", ) # 创建时间 created_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, default=datetime.now, comment="创建时间", ) # 更新时间 updated_at: Mapped[datetime] = mapped_column( DateTime(timezone=True), nullable=False, default=datetime.now, onupdate=datetime.now, comment="更新时间", ) # -------------------------------------------------------------------------- # 索引定义(和架构文档 DDL 严格一致) # -------------------------------------------------------------------------- __table_args__ = ( # 按状态查询(如查询所有排队中的会话) Index("idx_conversations_status", "status"), # 按员工ID查询(如查询某个员工的所有会话) Index("idx_conversations_employee_id", "employee_id"), # US-7: 按企业ID查询(如查询某企业所有会话) Index("idx_conversations_corp_id", "corp_id"), # 按坐席ID查询(如查询某个坐席正在服务的所有会话) Index("idx_conversations_assigned_agent", "assigned_agent_id"), # 按紧急度倒序查询(紧急度高的排前面) Index("idx_conversations_urgency_score", "urgency_score"), # 按最后消息时间倒序查询(最新消息的排前面) Index("idx_conversations_last_message_at", "last_message_at"), ) def __repr__(self) -> str: """会话对象的字符串表示,方便调试。""" return ( f"" )