You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
gcgj-dify-1.7.0/api/services/end_user_service.py

314 lines
11 KiB
Python

from typing import Any, Optional
from sqlalchemy import and_, desc, func
from extensions.ext_database import db
from libs.infinite_scroll_pagination import MultiPagePagination
from models.account import Account
from models.model import App, Conversation, EndUser, Message
from services.organization_service import OrganizationService
class EndUserService:
@staticmethod
def pagination_by_filters(
app_model: App, filters: dict[str, Any], offset: int, limit: int, organization_id: Optional[str] = None
) -> MultiPagePagination:
"""
Get a list of end users with filtering and pagination
Args:
app_model: The app model
filters: Dictionary containing filter criteria
offset: Number of records to skip
limit: Maximum number of records to return
organization_id: Optional organization ID to filter users by
Returns:
Dictionary containing total count and list of end users
"""
# Get message data to calculate active days
message_days_subq = (
db.session.query(
Message.from_end_user_id,
func.count(
func.distinct(func.date(func.timezone("UTC+8", func.timezone("UTC", Message.created_at))))
).label("active_days"),
)
.filter(Message.app_id == app_model.id)
.group_by(Message.from_end_user_id)
.subquery()
)
# Start with base query - using join and subqueries for the chat timing data
subq = (
db.session.query(
Conversation.from_end_user_id,
func.max(Conversation.created_at).label("last_chat_at"),
func.min(Conversation.created_at).label("first_chat_at"),
func.count(Message.id).label("total_messages"),
)
.filter(Conversation.app_id == app_model.id)
.join(Message, Message.conversation_id == Conversation.id)
.group_by(Conversation.from_end_user_id)
.subquery()
)
query = (
db.session.query(
EndUser,
subq.c.last_chat_at,
subq.c.first_chat_at,
subq.c.total_messages,
message_days_subq.c.active_days,
)
.outerjoin(subq, EndUser.id == subq.c.from_end_user_id)
.outerjoin(message_days_subq, EndUser.id == message_days_subq.c.from_end_user_id)
.filter(EndUser.app_id == app_model.id)
.filter(EndUser.external_user_id != None)
)
# Filter by organization if specified
if organization_id:
query = query.filter(EndUser.organization_id == organization_id)
# Apply filters
filter_conditions = []
if "health_status" in filters:
filter_conditions.append(EndUser.health_status == filters["health_status"])
if "last_chat_at__gte" in filters:
filter_conditions.append(subq.c.last_chat_at >= filters["last_chat_at__gte"])
if "last_chat_at__lte" in filters:
filter_conditions.append(subq.c.last_chat_at <= filters["last_chat_at__lte"])
# Apply all filter conditions
if filter_conditions:
query = query.filter(and_(*filter_conditions))
# Get total count before applying pagination
total_count = query.count()
# Apply pagination - now ordering by the joined column
query = query.order_by(desc(subq.c.last_chat_at))
query = query.offset(offset).limit(limit)
# Execute query
results = query.all()
# Process results to include the chat timing data
users = []
for result in results:
end_user = result[0]
end_user.last_chat_at = result[1]
end_user.first_chat_at = result[2]
end_user.total_messages = result[3] if result[3] is not None else 0
end_user.active_days = result[4] if result[4] is not None else 0
# Convert to dictionary for JSON serialization
end_user_dict = {
"id": end_user.external_user_id,
"email": end_user.email,
"first_chat_at": end_user.first_chat_at,
"last_chat_at": end_user.last_chat_at,
"total_messages": end_user.total_messages,
"active_days": end_user.active_days,
"health_status": end_user.health_status,
"topics": end_user.topics,
"summary": end_user.summary,
"major": end_user.major,
"organization_id": end_user.organization_id,
}
users.append(end_user_dict)
# Format and return results
return MultiPagePagination(data=users, total=total_count)
@staticmethod
def load_end_user_by_id(end_user_id: str) -> EndUser:
return db.session.query(EndUser).filter(EndUser.external_user_id == end_user_id).first()
@staticmethod
def get_user_profile(end_user_id: str) -> dict[str, Any]:
"""
Get user profile information
Args:
end_user_id: The ID of the end user
Returns:
Dict containing user profile information
"""
# Get EndUser information
end_user = db.session.query(EndUser).filter(EndUser.external_user_id == end_user_id).first()
if not end_user:
return {"username": None, "gender": "unknown", "major": None, "email": None}
# Map numeric gender to string representation
gender_map = {0: "unknown", 1: "male", 2: "female"}
return {
"username": end_user.name,
"gender": gender_map.get(end_user.gender, "unknown"),
"major": end_user.major,
"email": end_user.email,
}
@staticmethod
def update_user_profile(end_user: EndUser, profile_data: dict[str, Any]) -> tuple[bool, Optional[str]]:
"""
Update user profile information
Args:
end_user: The EndUser object to update
profile_data: Dictionary containing profile data to update
Returns:
Tuple of (success, error_message)
"""
try:
# Update username if provided
if "username" in profile_data:
end_user.name = profile_data["username"]
# Update gender if provided
if "gender" in profile_data:
gender_str = profile_data["gender"]
gender_map = {"unknown": 0, "male": 1, "female": 2}
end_user.gender = gender_map[gender_str]
# Update major if provided
if "major" in profile_data:
major = profile_data["major"]
# Create a new dictionary if extra_profile is None
if end_user.extra_profile is None:
end_user.extra_profile = {}
# Make a copy of the existing dictionary to ensure changes are detected
extra_profile = dict(end_user.extra_profile)
extra_profile["major"] = major
end_user.extra_profile = extra_profile
# Force the change to be detected
db.session.add(end_user)
# Save changes to database
db.session.commit()
return True, None
except Exception as e:
db.session.rollback()
return False, str(e)
@classmethod
def get_or_create_end_user(cls, app_model: App, user_id: str, user_type: str = "service_api_with_auth") -> EndUser:
"""
Get or create an end user with organization awareness
Args:
app_model: The app model
user_id: The external user ID (often an account ID)
user_type: The type of end user (default: service_api_with_auth)
Returns:
The end user
"""
if not user_id:
user_id = "DEFAULT-USER"
# Find existing end user
end_user = (
db.session.query(EndUser)
.filter(
EndUser.tenant_id == app_model.tenant_id,
EndUser.app_id == app_model.id,
EndUser.external_user_id == user_id,
EndUser.type == user_type,
)
.first()
)
# Get organization if the user has an account
organization_id = None
if user_id != "DEFAULT-USER":
account = db.session.query(Account).filter(Account.id == user_id).first()
if account:
organization = OrganizationService.get_organization_for_account_or_assign(account, app_model.tenant_id)
if organization:
organization_id = organization.id
if not end_user:
# Create new end user
end_user = EndUser(
tenant_id=app_model.tenant_id,
app_id=app_model.id,
type=user_type,
external_user_id=user_id,
session_id=user_id,
organization_id=organization_id,
)
db.session.add(end_user)
db.session.commit()
elif organization_id and end_user.organization_id != organization_id:
# Update organization if needed
OrganizationService.assign_end_user_to_organization(end_user, organization_id)
return end_user
@classmethod
def get_organization_for_end_user(cls, end_user: EndUser) -> Optional[dict]:
"""
Get organization info for an end user
Args:
end_user: The end user
Returns:
Organization info as dict or None
"""
if not end_user or not end_user.organization_id:
return None
organization = OrganizationService.get_organization_by_id(end_user.organization_id)
if organization:
return {
"id": organization.id,
"name": organization.name,
"code": organization.code,
"type": organization.type,
}
return None
@classmethod
def update_end_user(
cls, end_user: EndUser, name: Optional[str] = None, organization_id: Optional[str] = None
) -> EndUser:
"""
Update an end user's properties
Args:
end_user: The end user to update
name: New name (optional)
organization_id: New organization ID (optional)
Returns:
The updated end user
"""
if not end_user:
raise ValueError("End user cannot be None")
if name:
end_user.name = name
if organization_id and end_user.organization_id != organization_id:
OrganizationService.assign_end_user_to_organization(end_user, organization_id)
db.session.commit()
return end_user