feat: add admin user detail insights

This commit is contained in:
2026-07-31 16:30:39 +08:00
parent 763c78bd7e
commit 88ed92d78c
6 changed files with 361 additions and 5 deletions

View File

@@ -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 {}