#!/usr/bin/env python3 # -*- coding: utf-8 -*- """ API使用統計資料模型 Author: PANJIT IT Team Created: 2024-01-28 Modified: 2024-01-28 """ from datetime import datetime, timedelta from sqlalchemy.sql import func from app import db class APIUsageStats(db.Model): """API使用統計表 (dt_api_usage_stats)""" __tablename__ = 'dt_api_usage_stats' id = db.Column(db.Integer, primary_key=True, autoincrement=True) user_id = db.Column(db.Integer, db.ForeignKey('dt_users.id'), nullable=False, comment='使用者ID') job_id = db.Column(db.Integer, db.ForeignKey('dt_translation_jobs.id'), comment='任務ID') api_endpoint = db.Column(db.String(200), nullable=False, comment='API端點') prompt_tokens = db.Column(db.Integer, default=0, comment='Prompt token數') completion_tokens = db.Column(db.Integer, default=0, comment='Completion token數') total_tokens = db.Column(db.Integer, default=0, comment='總token數') prompt_unit_price = db.Column(db.Numeric(10, 8), default=0.00000000, comment='單價') prompt_price_unit = db.Column(db.String(20), default='USD', comment='價格單位') cost = db.Column(db.Numeric(10, 4), default=0.0000, comment='成本') response_time_ms = db.Column(db.Integer, default=0, comment='回應時間(毫秒)') success = db.Column(db.Boolean, default=True, comment='是否成功') error_message = db.Column(db.Text, comment='錯誤訊息') created_at = db.Column(db.DateTime, default=func.now(), comment='建立時間') def __repr__(self): return f'' def to_dict(self): """轉換為字典格式""" return { 'id': self.id, 'user_id': self.user_id, 'job_id': self.job_id, 'api_endpoint': self.api_endpoint, 'prompt_tokens': self.prompt_tokens, 'completion_tokens': self.completion_tokens, 'total_tokens': self.total_tokens, 'prompt_unit_price': float(self.prompt_unit_price) if self.prompt_unit_price else 0.0, 'prompt_price_unit': self.prompt_price_unit, 'cost': float(self.cost) if self.cost else 0.0, 'response_time_ms': self.response_time_ms, 'success': self.success, 'error_message': self.error_message, 'created_at': self.created_at.isoformat() if self.created_at else None } @classmethod def record_api_call(cls, user_id, job_id, api_endpoint, metadata, response_time_ms, success=True, error_message=None): """記錄 API 呼叫統計""" # 從 Dify API metadata 解析使用量資訊 usage_data = metadata.get('usage', {}) prompt_tokens = usage_data.get('prompt_tokens', 0) completion_tokens = usage_data.get('completion_tokens', 0) total_tokens = usage_data.get('total_tokens', prompt_tokens + completion_tokens) # 計算成本 - 使用 Dify API 提供的總成本 if 'total_price' in usage_data: # 直接使用 API 提供的總價格 cost = float(usage_data.get('total_price', 0.0)) else: # 備用計算方式 prompt_price = float(usage_data.get('prompt_price', 0.0)) completion_price = float(usage_data.get('completion_price', 0.0)) cost = prompt_price + completion_price # 單價資訊 prompt_unit_price = usage_data.get('prompt_unit_price', 0.0) completion_unit_price = usage_data.get('completion_unit_price', 0.0) prompt_price_unit = usage_data.get('currency', 'USD') stats = cls( user_id=user_id, job_id=job_id, api_endpoint=api_endpoint, prompt_tokens=prompt_tokens, completion_tokens=completion_tokens, total_tokens=total_tokens, prompt_unit_price=prompt_unit_price, prompt_price_unit=prompt_price_unit, cost=cost, response_time_ms=response_time_ms, success=success, error_message=error_message ) db.session.add(stats) db.session.commit() return stats @classmethod def get_user_statistics(cls, user_id, start_date=None, end_date=None): """取得使用者統計資料""" query = cls.query.filter_by(user_id=user_id) if start_date: query = query.filter(cls.created_at >= start_date) if end_date: query = query.filter(cls.created_at <= end_date) # 統計資料 total_calls = query.count() successful_calls = query.filter_by(success=True).count() total_tokens = query.with_entities(func.sum(cls.total_tokens)).scalar() or 0 total_cost = query.with_entities(func.sum(cls.cost)).scalar() or 0.0 avg_response_time = query.with_entities(func.avg(cls.response_time_ms)).scalar() or 0 return { 'total_calls': total_calls, 'successful_calls': successful_calls, 'failed_calls': total_calls - successful_calls, 'success_rate': (successful_calls / total_calls * 100) if total_calls > 0 else 0, 'total_tokens': total_tokens, 'total_cost': float(total_cost), 'avg_response_time': float(avg_response_time) if avg_response_time else 0 } @classmethod def get_daily_statistics(cls, days=30): """取得每日統計資料""" end_date = datetime.utcnow() start_date = end_date - timedelta(days=days) # 按日期分組統計 daily_stats = db.session.query( func.date(cls.created_at).label('date'), func.count(cls.id).label('total_calls'), func.sum(cls.total_tokens).label('total_tokens'), func.sum(cls.cost).label('total_cost'), func.count().filter(cls.success == True).label('successful_calls') ).filter( cls.created_at >= start_date, cls.created_at <= end_date ).group_by(func.date(cls.created_at)).all() return [ { 'date': stat.date.isoformat(), 'total_calls': stat.total_calls, 'successful_calls': stat.successful_calls, 'failed_calls': stat.total_calls - stat.successful_calls, 'total_tokens': stat.total_tokens or 0, 'total_cost': float(stat.total_cost or 0) } for stat in daily_stats ] @classmethod def get_top_users(cls, limit=10, start_date=None, end_date=None): """取得使用量排行榜""" query = db.session.query( cls.user_id, func.count(cls.id).label('total_calls'), func.sum(cls.total_tokens).label('total_tokens'), func.sum(cls.cost).label('total_cost') ) if start_date: query = query.filter(cls.created_at >= start_date) if end_date: query = query.filter(cls.created_at <= end_date) top_users = query.group_by(cls.user_id).order_by( func.sum(cls.cost).desc() ).limit(limit).all() return [ { 'user_id': user.user_id, 'total_calls': user.total_calls, 'total_tokens': user.total_tokens or 0, 'total_cost': float(user.total_cost or 0) } for user in top_users ] @classmethod def get_cost_trend(cls, days=30): """取得成本趨勢""" end_date = datetime.utcnow() start_date = end_date - timedelta(days=days) # 按日期統計成本 cost_trend = db.session.query( func.date(cls.created_at).label('date'), func.sum(cls.cost).label('daily_cost') ).filter( cls.created_at >= start_date, cls.created_at <= end_date ).group_by(func.date(cls.created_at)).order_by( func.date(cls.created_at) ).all() return [ { 'date': trend.date.isoformat(), 'cost': float(trend.daily_cost or 0) } for trend in cost_trend ] @classmethod def get_endpoint_statistics(cls): """取得 API 端點統計""" endpoint_stats = db.session.query( cls.api_endpoint, func.count(cls.id).label('total_calls'), func.sum(cls.cost).label('total_cost'), func.avg(cls.response_time_ms).label('avg_response_time') ).group_by(cls.api_endpoint).order_by( func.count(cls.id).desc() ).all() return [ { 'endpoint': stat.api_endpoint, 'total_calls': stat.total_calls, 'total_cost': float(stat.total_cost or 0), 'avg_response_time': float(stat.avg_response_time or 0) } for stat in endpoint_stats ]