feat(models): 组织、用户、角色、客户、会话与消息模型
按组织边界定义数据关系,补齐仓储层与通用工具,迁移脚本可重复执行。
This commit is contained in:
1 parent
977783c126
commit
9eb8613c05
126 files changed
+17436
No files matched your search
@@ -0,0 +1,109 @@
|
||||
"""
|
||||
Copyright 2024-2026 ReplyPier
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
"""
|
||||
|
||||
from sqlalchemy import Column, String, DateTime, ForeignKey, Enum as SQLEnum, JSON, Float
|
||||
from sqlalchemy.orm import relationship
|
||||
from sqlalchemy.dialects.postgresql import UUID
|
||||
from app.core.encryption import EncryptedText
|
||||
from app.database import Base
|
||||
import enum
|
||||
from sqlalchemy.sql import func
|
||||
from app.models.user import UserGroup
|
||||
|
||||
|
||||
class SessionStatus(str, enum.Enum):
|
||||
OPEN = "open"
|
||||
TRANSFERRED = "transferred"
|
||||
CLOSED = "closed"
|
||||
|
||||
|
||||
class EndChatReasonType(str, enum.Enum):
|
||||
ISSUE_RESOLVED = "ISSUE_RESOLVED"
|
||||
CUSTOMER_REQUEST = "CUSTOMER_REQUEST"
|
||||
CONFIRMATION_RECEIVED = "CONFIRMATION_RECEIVED"
|
||||
FAREWELL = "FAREWELL"
|
||||
THANK_YOU = "THANK_YOU"
|
||||
NATURAL_CONCLUSION = "NATURAL_CONCLUSION"
|
||||
TASK_COMPLETED = "TASK_COMPLETED"
|
||||
|
||||
|
||||
# A conversation the visitor can still talk in.
|
||||
ACTIVE_STATUSES = (SessionStatus.OPEN, SessionStatus.TRANSFERRED)
|
||||
|
||||
|
||||
class SessionToAgent(Base):
|
||||
__tablename__ = "session_to_agents"
|
||||
|
||||
session_id = Column(UUID(as_uuid=True), primary_key=True)
|
||||
user_id = Column(UUID(as_uuid=True), ForeignKey("users.id", ondelete="SET NULL"), nullable=True)
|
||||
agent_id = Column(UUID(as_uuid=True), ForeignKey("agents.id", ondelete="SET NULL"), nullable=True)
|
||||
customer_id = Column(UUID(as_uuid=True), ForeignKey("customers.id", ondelete="SET NULL"), nullable=False, index=True)
|
||||
group_id = Column(UUID(as_uuid=True), ForeignKey("groups.id", ondelete="SET NULL"), nullable=True)
|
||||
organization_id = Column(UUID(as_uuid=True), ForeignKey("organizations.id", ondelete="CASCADE"), nullable=False)
|
||||
status = Column(SQLEnum(SessionStatus), nullable=False, default=SessionStatus.OPEN)
|
||||
# Messaging channel the session belongs to ('web' = widget, 'telegram',
|
||||
# 'whatsapp', ...). Routing key for outbound delivery of replies.
|
||||
channel = Column(String, nullable=False, server_default='web', index=True)
|
||||
# Which widget conversation token may resume this session (see
|
||||
# conversation_token.conversation_scope). NULL: only a token the embedding app
|
||||
# vouched for - and every non-widget channel, which never resumes by token.
|
||||
conversation_key = Column(String(64), nullable=True)
|
||||
assigned_at = Column(DateTime(timezone=True), server_default=func.now())
|
||||
|
||||
updated_at = Column(DateTime(timezone=True), server_default=func.now(), onupdate=func.now())
|
||||
transfer_reason = Column(String, nullable=True)
|
||||
# The free-text fields below are AI-written summaries of the conversation, so
|
||||
# they are encrypted at rest alongside the messages themselves.
|
||||
transfer_description = Column(EncryptedText, nullable=True)
|
||||
end_chat_reason = Column(SQLEnum(EndChatReasonType), nullable=True)
|
||||
end_chat_description = Column(EncryptedText, nullable=True)
|
||||
|
||||
# Ticket-related fields
|
||||
ticket_id = Column(String, nullable=True)
|
||||
ticket_status = Column(String, nullable=True)
|
||||
ticket_summary = Column(EncryptedText, nullable=True)
|
||||
ticket_description = Column(EncryptedText, nullable=True)
|
||||
integration_type = Column(String, nullable=True)
|
||||
ticket_priority = Column(String, nullable=True)
|
||||
|
||||
# Sentiment fields
|
||||
sentiment_label = Column(String, nullable=True) # Overall session sentiment: 'positive', 'neutral', 'negative'
|
||||
sentiment_score = Column(Float, nullable=True) # Overall average sentiment score: -1.0 to 1.0
|
||||
|
||||
# Workflow-related fields
|
||||
workflow_id = Column(UUID(as_uuid=True), ForeignKey("workflows.id", ondelete="SET NULL"), nullable=True)
|
||||
current_node_id = Column(UUID(as_uuid=True), ForeignKey("workflow_nodes.id", ondelete="SET NULL"), nullable=True)
|
||||
|
||||
workflow_state = Column(JSON, default={}) # Store workflow execution state
|
||||
workflow_history = Column(JSON, default=[]) # Store form submissions and workflow interactions
|
||||
|
||||
# Relationships
|
||||
user = relationship("User", back_populates="session_assignments")
|
||||
agent = relationship("Agent", back_populates="session_assignments")
|
||||
customer = relationship("Customer", back_populates="session_assignments")
|
||||
group = relationship("UserGroup", back_populates="session_assignments")
|
||||
ratings = relationship("Rating", back_populates="session_assignments")
|
||||
workflow = relationship("Workflow", back_populates="sessions")
|
||||
current_node = relationship("WorkflowNode")
|
||||
|
||||
|
||||
# Add back-reference in UserGroup model if not already present
|
||||
if not hasattr(UserGroup, 'session_assignments'):
|
||||
UserGroup.session_assignments = relationship(
|
||||
"SessionToAgent",
|
||||
back_populates="group",
|
||||
uselist=True
|
||||
)
|
||||
Reference in new issue
Block a user