feat: add admin user detail insights
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import re
|
||||
from datetime import UTC, date, datetime
|
||||
from datetime import UTC, date, datetime, timedelta
|
||||
from io import BytesIO
|
||||
|
||||
from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile, status
|
||||
@@ -17,11 +17,16 @@ from app.core.responses import api_success
|
||||
from app.models.admin import Admin
|
||||
from app.models.ai_config import SystemConfig
|
||||
from app.models.entitlement import EntitlementPlan, UserEntitlement
|
||||
from app.models.chat import TopicSession
|
||||
from app.models.chat import ChatMessage, ChatSession, TopicSession
|
||||
from app.models.growth import ShareDraft, TeacherHelpCard, TopicSummary
|
||||
from app.models.logs import AiRequestLog
|
||||
from app.models.user import User
|
||||
from app.schemas.admin import AdminUserCreateRequest, AdminUserImportItem, AdminUserImportRequest, AdminUserUpdateRequest
|
||||
from app.services.admin_service import OperationLogService
|
||||
from app.services.entitlement_service import EntitlementService, entitlement_dict, view_from_plan
|
||||
from app.services.growth_profile_service import GrowthProfileService, growth_profile_dict, topic_dict, topic_summary_dict
|
||||
from app.services.help_card_service import help_card_dict
|
||||
from app.services.share_draft_service import share_draft_dict
|
||||
from app.api.pagination import page_result
|
||||
|
||||
router = APIRouter()
|
||||
@@ -234,6 +239,30 @@ def user_detail(
|
||||
return api_success(_user_dict(user))
|
||||
|
||||
|
||||
@router.get("/user/{user_id}/detail")
|
||||
def user_operation_detail(
|
||||
user_id: int,
|
||||
db: Session = Depends(get_db),
|
||||
current_admin: Admin = Depends(get_current_admin),
|
||||
) -> dict:
|
||||
user = _get_user(db, user_id)
|
||||
entitlement = EntitlementService.active_entitlement(
|
||||
db,
|
||||
user,
|
||||
monthly_topic_used=_monthly_topic_counts(db, [user.id]).get(user.id, 0),
|
||||
)
|
||||
return api_success(
|
||||
{
|
||||
"user": _user_dict(user, entitlement_dict(entitlement)),
|
||||
"metrics": _user_metrics(db, user=user, monthly_topic_limit=entitlement.monthly_topic_limit),
|
||||
"growthProfile": growth_profile_dict(GrowthProfileService.get_growth_profile(db, user.id)),
|
||||
"recentTopics": _recent_topics(db, user.id),
|
||||
"recentHelpCards": [help_card_dict(item) for item in _recent_help_cards(db, user.id)],
|
||||
"recentShareDrafts": [share_draft_dict(item) for item in _recent_share_drafts(db, user.id)],
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
@router.put("/user/{user_id}")
|
||||
def update_user(
|
||||
user_id: int,
|
||||
@@ -345,6 +374,95 @@ def _active_entitlement_rows(db: Session, user_ids: list[int]) -> dict[int, tupl
|
||||
return result
|
||||
|
||||
|
||||
def _user_metrics(db: Session, *, user: User, monthly_topic_limit: int | None) -> dict:
|
||||
now = datetime.now(UTC).replace(tzinfo=None)
|
||||
month_start = now.replace(day=1, hour=0, minute=0, second=0, microsecond=0)
|
||||
recent_start = now - timedelta(days=30)
|
||||
total_sessions = db.scalar(select(func.count(ChatSession.id)).where(ChatSession.user_id == user.id, ChatSession.is_deleted == 0)) or 0
|
||||
total_messages = db.scalar(select(func.count(ChatMessage.id)).where(ChatMessage.user_id == user.id)) or 0
|
||||
total_topics = db.scalar(select(func.count(TopicSession.id)).where(TopicSession.user_id == user.id)) or 0
|
||||
month_topics = db.scalar(
|
||||
select(func.count(TopicSession.id)).where(TopicSession.user_id == user.id, TopicSession.started_at >= month_start)
|
||||
) or 0
|
||||
recent_active_days = db.scalar(
|
||||
select(func.count(func.distinct(func.date(ChatMessage.created_at)))).where(
|
||||
ChatMessage.user_id == user.id,
|
||||
ChatMessage.created_at >= recent_start,
|
||||
)
|
||||
) or 0
|
||||
last_message_at = db.scalar(select(func.max(ChatMessage.created_at)).where(ChatMessage.user_id == user.id))
|
||||
token_row = db.execute(
|
||||
select(
|
||||
func.coalesce(func.sum(AiRequestLog.input_token), 0),
|
||||
func.coalesce(func.sum(AiRequestLog.output_token), 0),
|
||||
func.coalesce(func.sum(AiRequestLog.total_token), 0),
|
||||
func.coalesce(func.sum(AiRequestLog.estimated_cost), 0),
|
||||
func.max(AiRequestLog.currency),
|
||||
func.count(AiRequestLog.id),
|
||||
).where(AiRequestLog.user_id == user.id)
|
||||
).one()
|
||||
help_card_count = db.scalar(select(func.count(TeacherHelpCard.id)).where(TeacherHelpCard.user_id == user.id)) or 0
|
||||
share_draft_count = db.scalar(select(func.count(ShareDraft.id)).where(ShareDraft.user_id == user.id)) or 0
|
||||
usage_ratio = (month_topics / monthly_topic_limit) if monthly_topic_limit else None
|
||||
return {
|
||||
"totalSessions": int(total_sessions),
|
||||
"totalMessages": int(total_messages),
|
||||
"totalTopics": int(total_topics),
|
||||
"monthTopics": int(month_topics),
|
||||
"recentActiveDays": int(recent_active_days),
|
||||
"lastMessageAt": last_message_at,
|
||||
"aiRequestCount": int(token_row[5] or 0),
|
||||
"inputToken": int(token_row[0] or 0),
|
||||
"outputToken": int(token_row[1] or 0),
|
||||
"totalToken": int(token_row[2] or 0),
|
||||
"estimatedCost": float(token_row[3] or 0),
|
||||
"costCurrency": token_row[4] or "CNY",
|
||||
"helpCardCount": int(help_card_count),
|
||||
"shareDraftCount": int(share_draft_count),
|
||||
"isHighFrequency": bool(usage_ratio is not None and usage_ratio >= 0.8) or recent_active_days >= 15,
|
||||
"isInactive": total_sessions == 0 or last_message_at is None or last_message_at < recent_start,
|
||||
}
|
||||
|
||||
|
||||
def _recent_topics(db: Session, user_id: int, *, limit: int = 10) -> list[dict]:
|
||||
rows = db.execute(
|
||||
select(TopicSession, TopicSummary)
|
||||
.join(TopicSummary, TopicSummary.topic_session_id == TopicSession.id, isouter=True)
|
||||
.where(TopicSession.user_id == user_id)
|
||||
.order_by(TopicSession.updated_at.desc(), TopicSession.id.desc())
|
||||
.limit(limit)
|
||||
).all()
|
||||
return [
|
||||
{
|
||||
**topic_dict(topic),
|
||||
"summary": topic_summary_dict(summary) if summary else None,
|
||||
}
|
||||
for topic, summary in rows
|
||||
]
|
||||
|
||||
|
||||
def _recent_help_cards(db: Session, user_id: int, *, limit: int = 10) -> list[TeacherHelpCard]:
|
||||
return list(
|
||||
db.scalars(
|
||||
select(TeacherHelpCard)
|
||||
.where(TeacherHelpCard.user_id == user_id)
|
||||
.order_by(TeacherHelpCard.created_at.desc(), TeacherHelpCard.id.desc())
|
||||
.limit(limit)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _recent_share_drafts(db: Session, user_id: int, *, limit: int = 10) -> list[ShareDraft]:
|
||||
return list(
|
||||
db.scalars(
|
||||
select(ShareDraft)
|
||||
.where(ShareDraft.user_id == user_id)
|
||||
.order_by(ShareDraft.created_at.desc(), ShareDraft.id.desc())
|
||||
.limit(limit)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def _monthly_topic_counts(db: Session, user_ids: list[int]) -> dict[int, int]:
|
||||
if not user_ids:
|
||||
return {}
|
||||
|
||||
Reference in New Issue
Block a user