1st_fix_login_issue
This commit is contained in:
24
app/models/__init__.py
Normal file
24
app/models/__init__.py
Normal file
@@ -0,0 +1,24 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
資料模型模組
|
||||
|
||||
Author: PANJIT IT Team
|
||||
Created: 2024-01-28
|
||||
Modified: 2024-01-28
|
||||
"""
|
||||
|
||||
from .user import User
|
||||
from .job import TranslationJob, JobFile
|
||||
from .cache import TranslationCache
|
||||
from .stats import APIUsageStats
|
||||
from .log import SystemLog
|
||||
|
||||
__all__ = [
|
||||
'User',
|
||||
'TranslationJob',
|
||||
'JobFile',
|
||||
'TranslationCache',
|
||||
'APIUsageStats',
|
||||
'SystemLog'
|
||||
]
|
138
app/models/cache.py
Normal file
138
app/models/cache.py
Normal file
@@ -0,0 +1,138 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
翻譯快取資料模型
|
||||
|
||||
Author: PANJIT IT Team
|
||||
Created: 2024-01-28
|
||||
Modified: 2024-01-28
|
||||
"""
|
||||
|
||||
import hashlib
|
||||
from sqlalchemy.sql import func
|
||||
from app import db
|
||||
|
||||
|
||||
class TranslationCache(db.Model):
|
||||
"""翻譯快取表 (dt_translation_cache)"""
|
||||
__tablename__ = 'dt_translation_cache'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
source_text_hash = db.Column(db.String(64), nullable=False, comment='來源文字hash')
|
||||
source_language = db.Column(db.String(50), nullable=False, comment='來源語言')
|
||||
target_language = db.Column(db.String(50), nullable=False, comment='目標語言')
|
||||
source_text = db.Column(db.Text, nullable=False, comment='來源文字')
|
||||
translated_text = db.Column(db.Text, nullable=False, comment='翻譯文字')
|
||||
created_at = db.Column(db.DateTime, default=func.now(), comment='建立時間')
|
||||
|
||||
# 唯一約束
|
||||
__table_args__ = (
|
||||
db.UniqueConstraint('source_text_hash', 'source_language', 'target_language', name='uk_cache'),
|
||||
db.Index('idx_languages', 'source_language', 'target_language'),
|
||||
)
|
||||
|
||||
def __repr__(self):
|
||||
return f'<TranslationCache {self.source_text_hash[:8]}...>'
|
||||
|
||||
def to_dict(self):
|
||||
"""轉換為字典格式"""
|
||||
return {
|
||||
'id': self.id,
|
||||
'source_text_hash': self.source_text_hash,
|
||||
'source_language': self.source_language,
|
||||
'target_language': self.target_language,
|
||||
'source_text': self.source_text,
|
||||
'translated_text': self.translated_text,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None
|
||||
}
|
||||
|
||||
@staticmethod
|
||||
def generate_hash(text):
|
||||
"""生成文字的 SHA256 hash"""
|
||||
return hashlib.sha256(text.encode('utf-8')).hexdigest()
|
||||
|
||||
@classmethod
|
||||
def get_translation(cls, source_text, source_language, target_language):
|
||||
"""取得快取的翻譯"""
|
||||
text_hash = cls.generate_hash(source_text)
|
||||
|
||||
cache_entry = cls.query.filter_by(
|
||||
source_text_hash=text_hash,
|
||||
source_language=source_language,
|
||||
target_language=target_language
|
||||
).first()
|
||||
|
||||
return cache_entry.translated_text if cache_entry else None
|
||||
|
||||
@classmethod
|
||||
def save_translation(cls, source_text, source_language, target_language, translated_text):
|
||||
"""儲存翻譯到快取"""
|
||||
text_hash = cls.generate_hash(source_text)
|
||||
|
||||
# 檢查是否已存在
|
||||
existing = cls.query.filter_by(
|
||||
source_text_hash=text_hash,
|
||||
source_language=source_language,
|
||||
target_language=target_language
|
||||
).first()
|
||||
|
||||
if existing:
|
||||
# 更新現有記錄
|
||||
existing.translated_text = translated_text
|
||||
else:
|
||||
# 建立新記錄
|
||||
cache_entry = cls(
|
||||
source_text_hash=text_hash,
|
||||
source_language=source_language,
|
||||
target_language=target_language,
|
||||
source_text=source_text,
|
||||
translated_text=translated_text
|
||||
)
|
||||
db.session.add(cache_entry)
|
||||
|
||||
db.session.commit()
|
||||
return True
|
||||
|
||||
@classmethod
|
||||
def get_cache_statistics(cls):
|
||||
"""取得快取統計資料"""
|
||||
total_entries = cls.query.count()
|
||||
|
||||
# 按語言對統計
|
||||
language_pairs = db.session.query(
|
||||
cls.source_language,
|
||||
cls.target_language,
|
||||
func.count(cls.id).label('count')
|
||||
).group_by(cls.source_language, cls.target_language).all()
|
||||
|
||||
# 最近一週的快取命中
|
||||
from datetime import datetime, timedelta
|
||||
week_ago = datetime.utcnow() - timedelta(days=7)
|
||||
recent_entries = cls.query.filter(cls.created_at >= week_ago).count()
|
||||
|
||||
return {
|
||||
'total_entries': total_entries,
|
||||
'language_pairs': [
|
||||
{
|
||||
'source_language': pair.source_language,
|
||||
'target_language': pair.target_language,
|
||||
'count': pair.count
|
||||
}
|
||||
for pair in language_pairs
|
||||
],
|
||||
'recent_entries': recent_entries
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def clear_old_cache(cls, days_to_keep=90):
|
||||
"""清理舊快取記錄"""
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
cutoff_date = datetime.utcnow() - timedelta(days=days_to_keep)
|
||||
|
||||
deleted_count = cls.query.filter(
|
||||
cls.created_at < cutoff_date
|
||||
).delete(synchronize_session=False)
|
||||
|
||||
db.session.commit()
|
||||
return deleted_count
|
268
app/models/job.py
Normal file
268
app/models/job.py
Normal file
@@ -0,0 +1,268 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
翻譯任務資料模型
|
||||
|
||||
Author: PANJIT IT Team
|
||||
Created: 2024-01-28
|
||||
Modified: 2024-01-28
|
||||
"""
|
||||
|
||||
import json
|
||||
import uuid
|
||||
from datetime import datetime, timedelta
|
||||
from sqlalchemy.sql import func
|
||||
from sqlalchemy import event
|
||||
from app import db
|
||||
|
||||
|
||||
class TranslationJob(db.Model):
|
||||
"""翻譯任務表 (dt_translation_jobs)"""
|
||||
__tablename__ = 'dt_translation_jobs'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
job_uuid = db.Column(db.String(36), unique=True, nullable=False, index=True, comment='任務唯一識別碼')
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('dt_users.id'), nullable=False, comment='使用者ID')
|
||||
original_filename = db.Column(db.String(500), nullable=False, comment='原始檔名')
|
||||
file_extension = db.Column(db.String(10), nullable=False, comment='檔案副檔名')
|
||||
file_size = db.Column(db.BigInteger, nullable=False, comment='檔案大小(bytes)')
|
||||
file_path = db.Column(db.String(1000), nullable=False, comment='檔案路徑')
|
||||
source_language = db.Column(db.String(50), default=None, comment='來源語言')
|
||||
target_languages = db.Column(db.JSON, nullable=False, comment='目標語言陣列')
|
||||
status = db.Column(
|
||||
db.Enum('PENDING', 'PROCESSING', 'COMPLETED', 'FAILED', 'RETRY', name='job_status'),
|
||||
default='PENDING',
|
||||
comment='任務狀態'
|
||||
)
|
||||
progress = db.Column(db.Numeric(5, 2), default=0.00, comment='處理進度(%)')
|
||||
retry_count = db.Column(db.Integer, default=0, comment='重試次數')
|
||||
error_message = db.Column(db.Text, comment='錯誤訊息')
|
||||
total_tokens = db.Column(db.Integer, default=0, comment='總token數')
|
||||
total_cost = db.Column(db.Numeric(10, 4), default=0.0000, comment='總成本')
|
||||
processing_started_at = db.Column(db.DateTime, comment='開始處理時間')
|
||||
completed_at = db.Column(db.DateTime, comment='完成時間')
|
||||
created_at = db.Column(db.DateTime, default=func.now(), comment='建立時間')
|
||||
updated_at = db.Column(
|
||||
db.DateTime,
|
||||
default=func.now(),
|
||||
onupdate=func.now(),
|
||||
comment='更新時間'
|
||||
)
|
||||
|
||||
# 關聯關係
|
||||
files = db.relationship('JobFile', backref='job', lazy='dynamic', cascade='all, delete-orphan')
|
||||
api_usage_stats = db.relationship('APIUsageStats', backref='job', lazy='dynamic')
|
||||
|
||||
def __repr__(self):
|
||||
return f'<TranslationJob {self.job_uuid}>'
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
"""初始化,自動生成 UUID"""
|
||||
super().__init__(**kwargs)
|
||||
if not self.job_uuid:
|
||||
self.job_uuid = str(uuid.uuid4())
|
||||
|
||||
def to_dict(self, include_files=False):
|
||||
"""轉換為字典格式"""
|
||||
data = {
|
||||
'id': self.id,
|
||||
'job_uuid': self.job_uuid,
|
||||
'user_id': self.user_id,
|
||||
'original_filename': self.original_filename,
|
||||
'file_extension': self.file_extension,
|
||||
'file_size': self.file_size,
|
||||
'file_path': self.file_path,
|
||||
'source_language': self.source_language,
|
||||
'target_languages': self.target_languages,
|
||||
'status': self.status,
|
||||
'progress': float(self.progress) if self.progress else 0.0,
|
||||
'retry_count': self.retry_count,
|
||||
'error_message': self.error_message,
|
||||
'total_tokens': self.total_tokens,
|
||||
'total_cost': float(self.total_cost) if self.total_cost else 0.0,
|
||||
'processing_started_at': self.processing_started_at.isoformat() if self.processing_started_at else None,
|
||||
'completed_at': self.completed_at.isoformat() if self.completed_at else None,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||
'updated_at': self.updated_at.isoformat() if self.updated_at else None
|
||||
}
|
||||
|
||||
if include_files:
|
||||
data['files'] = [f.to_dict() for f in self.files]
|
||||
|
||||
return data
|
||||
|
||||
def update_status(self, status, error_message=None, progress=None):
|
||||
"""更新任務狀態"""
|
||||
self.status = status
|
||||
|
||||
if error_message:
|
||||
self.error_message = error_message
|
||||
|
||||
if progress is not None:
|
||||
self.progress = progress
|
||||
|
||||
if status == 'PROCESSING' and not self.processing_started_at:
|
||||
self.processing_started_at = datetime.utcnow()
|
||||
elif status == 'COMPLETED':
|
||||
self.completed_at = datetime.utcnow()
|
||||
self.progress = 100.00
|
||||
|
||||
self.updated_at = datetime.utcnow()
|
||||
db.session.commit()
|
||||
|
||||
def add_original_file(self, filename, file_path, file_size):
|
||||
"""新增原始檔案記錄"""
|
||||
original_file = JobFile(
|
||||
job_id=self.id,
|
||||
file_type='ORIGINAL',
|
||||
filename=filename,
|
||||
file_path=file_path,
|
||||
file_size=file_size
|
||||
)
|
||||
db.session.add(original_file)
|
||||
db.session.commit()
|
||||
return original_file
|
||||
|
||||
def add_translated_file(self, language_code, filename, file_path, file_size):
|
||||
"""新增翻譯檔案記錄"""
|
||||
translated_file = JobFile(
|
||||
job_id=self.id,
|
||||
file_type='TRANSLATED',
|
||||
language_code=language_code,
|
||||
filename=filename,
|
||||
file_path=file_path,
|
||||
file_size=file_size
|
||||
)
|
||||
db.session.add(translated_file)
|
||||
db.session.commit()
|
||||
return translated_file
|
||||
|
||||
def get_translated_files(self):
|
||||
"""取得翻譯檔案"""
|
||||
return self.files.filter_by(file_type='TRANSLATED').all()
|
||||
|
||||
def get_original_file(self):
|
||||
"""取得原始檔案"""
|
||||
return self.files.filter_by(file_type='ORIGINAL').first()
|
||||
|
||||
def can_retry(self):
|
||||
"""是否可以重試"""
|
||||
return self.status in ['FAILED', 'RETRY'] and self.retry_count < 3
|
||||
|
||||
def increment_retry(self):
|
||||
"""增加重試次數"""
|
||||
self.retry_count += 1
|
||||
self.updated_at = datetime.utcnow()
|
||||
db.session.commit()
|
||||
|
||||
@classmethod
|
||||
def get_queue_position(cls, job_uuid):
|
||||
"""取得任務在佇列中的位置"""
|
||||
job = cls.query.filter_by(job_uuid=job_uuid).first()
|
||||
if not job:
|
||||
return None
|
||||
|
||||
position = cls.query.filter(
|
||||
cls.status == 'PENDING',
|
||||
cls.created_at < job.created_at
|
||||
).count()
|
||||
|
||||
return position + 1
|
||||
|
||||
@classmethod
|
||||
def get_pending_jobs(cls):
|
||||
"""取得所有等待處理的任務"""
|
||||
return cls.query.filter_by(status='PENDING').order_by(cls.created_at.asc()).all()
|
||||
|
||||
@classmethod
|
||||
def get_processing_jobs(cls):
|
||||
"""取得所有處理中的任務"""
|
||||
return cls.query.filter_by(status='PROCESSING').all()
|
||||
|
||||
@classmethod
|
||||
def get_user_jobs(cls, user_id, status=None, limit=None, offset=None):
|
||||
"""取得使用者的任務列表"""
|
||||
query = cls.query.filter_by(user_id=user_id)
|
||||
|
||||
if status and status != 'all':
|
||||
query = query.filter_by(status=status.upper())
|
||||
|
||||
query = query.order_by(cls.created_at.desc())
|
||||
|
||||
if limit:
|
||||
query = query.limit(limit)
|
||||
if offset:
|
||||
query = query.offset(offset)
|
||||
|
||||
return query.all()
|
||||
|
||||
@classmethod
|
||||
def get_statistics(cls, user_id=None, start_date=None, end_date=None):
|
||||
"""取得統計資料"""
|
||||
query = cls.query
|
||||
|
||||
if user_id:
|
||||
query = 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 = query.count()
|
||||
completed = query.filter_by(status='COMPLETED').count()
|
||||
failed = query.filter_by(status='FAILED').count()
|
||||
processing = query.filter_by(status='PROCESSING').count()
|
||||
pending = query.filter_by(status='PENDING').count()
|
||||
|
||||
return {
|
||||
'total': total,
|
||||
'completed': completed,
|
||||
'failed': failed,
|
||||
'processing': processing,
|
||||
'pending': pending,
|
||||
'success_rate': (completed / total * 100) if total > 0 else 0
|
||||
}
|
||||
|
||||
|
||||
class JobFile(db.Model):
|
||||
"""檔案記錄表 (dt_job_files)"""
|
||||
__tablename__ = 'dt_job_files'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
job_id = db.Column(db.Integer, db.ForeignKey('dt_translation_jobs.id'), nullable=False, comment='任務ID')
|
||||
file_type = db.Column(
|
||||
db.Enum('ORIGINAL', 'TRANSLATED', name='file_type'),
|
||||
nullable=False,
|
||||
comment='檔案類型'
|
||||
)
|
||||
language_code = db.Column(db.String(50), comment='語言代碼(翻譯檔案)')
|
||||
filename = db.Column(db.String(500), nullable=False, comment='檔案名稱')
|
||||
file_path = db.Column(db.String(1000), nullable=False, comment='檔案路徑')
|
||||
file_size = db.Column(db.BigInteger, nullable=False, comment='檔案大小')
|
||||
created_at = db.Column(db.DateTime, default=func.now(), comment='建立時間')
|
||||
|
||||
def __repr__(self):
|
||||
return f'<JobFile {self.filename}>'
|
||||
|
||||
def to_dict(self):
|
||||
"""轉換為字典格式"""
|
||||
return {
|
||||
'id': self.id,
|
||||
'job_id': self.job_id,
|
||||
'file_type': self.file_type,
|
||||
'language_code': self.language_code,
|
||||
'filename': self.filename,
|
||||
'file_path': self.file_path,
|
||||
'file_size': self.file_size,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None
|
||||
}
|
||||
|
||||
|
||||
# 事件監聽器:自動生成 UUID
|
||||
@event.listens_for(TranslationJob, 'before_insert')
|
||||
def receive_before_insert(mapper, connection, target):
|
||||
"""在插入前自動生成 UUID"""
|
||||
if not target.job_uuid:
|
||||
target.job_uuid = str(uuid.uuid4())
|
211
app/models/log.py
Normal file
211
app/models/log.py
Normal file
@@ -0,0 +1,211 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
系統日誌資料模型
|
||||
|
||||
Author: PANJIT IT Team
|
||||
Created: 2024-01-28
|
||||
Modified: 2024-01-28
|
||||
"""
|
||||
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
from sqlalchemy.sql import func
|
||||
from app import db
|
||||
|
||||
|
||||
class SystemLog(db.Model):
|
||||
"""系統日誌表 (dt_system_logs)"""
|
||||
__tablename__ = 'dt_system_logs'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
level = db.Column(
|
||||
db.Enum('DEBUG', 'INFO', 'WARNING', 'ERROR', 'CRITICAL', name='log_level'),
|
||||
nullable=False,
|
||||
comment='日誌等級'
|
||||
)
|
||||
module = db.Column(db.String(100), nullable=False, comment='模組名稱')
|
||||
user_id = db.Column(db.Integer, db.ForeignKey('dt_users.id'), comment='使用者ID')
|
||||
job_id = db.Column(db.Integer, db.ForeignKey('dt_translation_jobs.id'), comment='任務ID')
|
||||
message = db.Column(db.Text, nullable=False, comment='日誌訊息')
|
||||
extra_data = db.Column(db.JSON, comment='額外資料')
|
||||
created_at = db.Column(db.DateTime, default=func.now(), comment='建立時間')
|
||||
|
||||
def __repr__(self):
|
||||
return f'<SystemLog {self.level} {self.module}>'
|
||||
|
||||
def to_dict(self):
|
||||
"""轉換為字典格式"""
|
||||
return {
|
||||
'id': self.id,
|
||||
'level': self.level,
|
||||
'module': self.module,
|
||||
'user_id': self.user_id,
|
||||
'job_id': self.job_id,
|
||||
'message': self.message,
|
||||
'extra_data': self.extra_data,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def log(cls, level, module, message, user_id=None, job_id=None, extra_data=None):
|
||||
"""記錄日誌"""
|
||||
log_entry = cls(
|
||||
level=level.upper(),
|
||||
module=module,
|
||||
message=message,
|
||||
user_id=user_id,
|
||||
job_id=job_id,
|
||||
extra_data=extra_data
|
||||
)
|
||||
|
||||
db.session.add(log_entry)
|
||||
db.session.commit()
|
||||
return log_entry
|
||||
|
||||
@classmethod
|
||||
def debug(cls, module, message, user_id=None, job_id=None, extra_data=None):
|
||||
"""記錄除錯日誌"""
|
||||
return cls.log('DEBUG', module, message, user_id, job_id, extra_data)
|
||||
|
||||
@classmethod
|
||||
def info(cls, module, message, user_id=None, job_id=None, extra_data=None):
|
||||
"""記錄資訊日誌"""
|
||||
return cls.log('INFO', module, message, user_id, job_id, extra_data)
|
||||
|
||||
@classmethod
|
||||
def warning(cls, module, message, user_id=None, job_id=None, extra_data=None):
|
||||
"""記錄警告日誌"""
|
||||
return cls.log('WARNING', module, message, user_id, job_id, extra_data)
|
||||
|
||||
@classmethod
|
||||
def error(cls, module, message, user_id=None, job_id=None, extra_data=None):
|
||||
"""記錄錯誤日誌"""
|
||||
return cls.log('ERROR', module, message, user_id, job_id, extra_data)
|
||||
|
||||
@classmethod
|
||||
def critical(cls, module, message, user_id=None, job_id=None, extra_data=None):
|
||||
"""記錄嚴重錯誤日誌"""
|
||||
return cls.log('CRITICAL', module, message, user_id, job_id, extra_data)
|
||||
|
||||
@classmethod
|
||||
def get_logs(cls, level=None, module=None, user_id=None, start_date=None, end_date=None, limit=100, offset=0):
|
||||
"""查詢日誌"""
|
||||
query = cls.query
|
||||
|
||||
if level:
|
||||
query = query.filter_by(level=level.upper())
|
||||
|
||||
if module:
|
||||
query = query.filter(cls.module.like(f'%{module}%'))
|
||||
|
||||
if user_id:
|
||||
query = 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)
|
||||
|
||||
# 按時間倒序排列
|
||||
query = query.order_by(cls.created_at.desc())
|
||||
|
||||
if limit:
|
||||
query = query.limit(limit)
|
||||
if offset:
|
||||
query = query.offset(offset)
|
||||
|
||||
return query.all()
|
||||
|
||||
@classmethod
|
||||
def get_log_statistics(cls, days=7):
|
||||
"""取得日誌統計資料"""
|
||||
end_date = datetime.utcnow()
|
||||
start_date = end_date - timedelta(days=days)
|
||||
|
||||
# 按等級統計
|
||||
level_stats = db.session.query(
|
||||
cls.level,
|
||||
func.count(cls.id).label('count')
|
||||
).filter(
|
||||
cls.created_at >= start_date
|
||||
).group_by(cls.level).all()
|
||||
|
||||
# 按模組統計
|
||||
module_stats = db.session.query(
|
||||
cls.module,
|
||||
func.count(cls.id).label('count')
|
||||
).filter(
|
||||
cls.created_at >= start_date
|
||||
).group_by(cls.module).order_by(
|
||||
func.count(cls.id).desc()
|
||||
).limit(10).all()
|
||||
|
||||
# 每日統計
|
||||
daily_stats = db.session.query(
|
||||
func.date(cls.created_at).label('date'),
|
||||
cls.level,
|
||||
func.count(cls.id).label('count')
|
||||
).filter(
|
||||
cls.created_at >= start_date
|
||||
).group_by(
|
||||
func.date(cls.created_at), cls.level
|
||||
).order_by(
|
||||
func.date(cls.created_at)
|
||||
).all()
|
||||
|
||||
return {
|
||||
'level_stats': [
|
||||
{'level': stat.level, 'count': stat.count}
|
||||
for stat in level_stats
|
||||
],
|
||||
'module_stats': [
|
||||
{'module': stat.module, 'count': stat.count}
|
||||
for stat in module_stats
|
||||
],
|
||||
'daily_stats': [
|
||||
{
|
||||
'date': stat.date.isoformat(),
|
||||
'level': stat.level,
|
||||
'count': stat.count
|
||||
}
|
||||
for stat in daily_stats
|
||||
]
|
||||
}
|
||||
|
||||
@classmethod
|
||||
def cleanup_old_logs(cls, days_to_keep=30):
|
||||
"""清理舊日誌"""
|
||||
cutoff_date = datetime.utcnow() - timedelta(days=days_to_keep)
|
||||
|
||||
deleted_count = cls.query.filter(
|
||||
cls.created_at < cutoff_date
|
||||
).delete(synchronize_session=False)
|
||||
|
||||
db.session.commit()
|
||||
return deleted_count
|
||||
|
||||
@classmethod
|
||||
def get_error_summary(cls, days=1):
|
||||
"""取得錯誤摘要"""
|
||||
start_date = datetime.utcnow() - timedelta(days=days)
|
||||
|
||||
error_logs = cls.query.filter(
|
||||
cls.level.in_(['ERROR', 'CRITICAL']),
|
||||
cls.created_at >= start_date
|
||||
).order_by(cls.created_at.desc()).limit(50).all()
|
||||
|
||||
# 按模組分組錯誤
|
||||
error_by_module = {}
|
||||
for log in error_logs:
|
||||
module = log.module
|
||||
if module not in error_by_module:
|
||||
error_by_module[module] = []
|
||||
error_by_module[module].append(log.to_dict())
|
||||
|
||||
return {
|
||||
'total_errors': len(error_logs),
|
||||
'error_by_module': error_by_module,
|
||||
'recent_errors': [log.to_dict() for log in error_logs[:10]]
|
||||
}
|
222
app/models/stats.py
Normal file
222
app/models/stats.py
Normal file
@@ -0,0 +1,222 @@
|
||||
#!/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'<APIUsageStats {self.api_endpoint}>'
|
||||
|
||||
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 解析使用量資訊
|
||||
prompt_tokens = metadata.get('usage', {}).get('prompt_tokens', 0)
|
||||
completion_tokens = metadata.get('usage', {}).get('completion_tokens', 0)
|
||||
total_tokens = metadata.get('usage', {}).get('total_tokens', prompt_tokens + completion_tokens)
|
||||
|
||||
# 計算成本
|
||||
prompt_unit_price = metadata.get('usage', {}).get('prompt_unit_price', 0.0)
|
||||
prompt_price_unit = metadata.get('usage', {}).get('prompt_price_unit', 'USD')
|
||||
|
||||
# 成本計算:通常是 prompt_tokens * prompt_unit_price
|
||||
cost = prompt_tokens * float(prompt_unit_price) if prompt_unit_price else 0.0
|
||||
|
||||
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
|
||||
]
|
113
app/models/user.py
Normal file
113
app/models/user.py
Normal file
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
使用者資料模型
|
||||
|
||||
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 User(db.Model):
|
||||
"""使用者資訊表 (dt_users)"""
|
||||
__tablename__ = 'dt_users'
|
||||
|
||||
id = db.Column(db.Integer, primary_key=True, autoincrement=True)
|
||||
username = db.Column(db.String(100), unique=True, nullable=False, index=True, comment='AD帳號')
|
||||
display_name = db.Column(db.String(200), nullable=False, comment='顯示名稱')
|
||||
email = db.Column(db.String(255), nullable=False, index=True, comment='電子郵件')
|
||||
department = db.Column(db.String(100), comment='部門')
|
||||
is_admin = db.Column(db.Boolean, default=False, comment='是否為管理員')
|
||||
last_login = db.Column(db.DateTime, comment='最後登入時間')
|
||||
created_at = db.Column(db.DateTime, default=func.now(), comment='建立時間')
|
||||
updated_at = db.Column(
|
||||
db.DateTime,
|
||||
default=func.now(),
|
||||
onupdate=func.now(),
|
||||
comment='更新時間'
|
||||
)
|
||||
|
||||
# 關聯關係
|
||||
translation_jobs = db.relationship('TranslationJob', backref='user', lazy='dynamic', cascade='all, delete-orphan')
|
||||
api_usage_stats = db.relationship('APIUsageStats', backref='user', lazy='dynamic', cascade='all, delete-orphan')
|
||||
system_logs = db.relationship('SystemLog', backref='user', lazy='dynamic')
|
||||
|
||||
def __repr__(self):
|
||||
return f'<User {self.username}>'
|
||||
|
||||
def to_dict(self, include_stats=False):
|
||||
"""轉換為字典格式"""
|
||||
data = {
|
||||
'id': self.id,
|
||||
'username': self.username,
|
||||
'display_name': self.display_name,
|
||||
'email': self.email,
|
||||
'department': self.department,
|
||||
'is_admin': self.is_admin,
|
||||
'last_login': self.last_login.isoformat() if self.last_login else None,
|
||||
'created_at': self.created_at.isoformat() if self.created_at else None,
|
||||
'updated_at': self.updated_at.isoformat() if self.updated_at else None
|
||||
}
|
||||
|
||||
if include_stats:
|
||||
data.update({
|
||||
'total_jobs': self.translation_jobs.count(),
|
||||
'completed_jobs': self.translation_jobs.filter_by(status='COMPLETED').count(),
|
||||
'failed_jobs': self.translation_jobs.filter_by(status='FAILED').count(),
|
||||
'total_cost': self.get_total_cost()
|
||||
})
|
||||
|
||||
return data
|
||||
|
||||
def get_total_cost(self):
|
||||
"""計算使用者總成本"""
|
||||
return db.session.query(
|
||||
func.sum(self.api_usage_stats.cost)
|
||||
).scalar() or 0.0
|
||||
|
||||
def update_last_login(self):
|
||||
"""更新最後登入時間"""
|
||||
self.last_login = datetime.utcnow()
|
||||
db.session.commit()
|
||||
|
||||
@classmethod
|
||||
def get_or_create(cls, username, display_name, email, department=None):
|
||||
"""取得或建立使用者"""
|
||||
user = cls.query.filter_by(username=username).first()
|
||||
|
||||
if user:
|
||||
# 更新使用者資訊
|
||||
user.display_name = display_name
|
||||
user.email = email
|
||||
if department:
|
||||
user.department = department
|
||||
user.updated_at = datetime.utcnow()
|
||||
else:
|
||||
# 建立新使用者
|
||||
user = cls(
|
||||
username=username,
|
||||
display_name=display_name,
|
||||
email=email,
|
||||
department=department,
|
||||
is_admin=(email.lower() == 'ymirliu@panjit.com.tw') # 硬編碼管理員
|
||||
)
|
||||
db.session.add(user)
|
||||
|
||||
db.session.commit()
|
||||
return user
|
||||
|
||||
@classmethod
|
||||
def get_admin_users(cls):
|
||||
"""取得所有管理員使用者"""
|
||||
return cls.query.filter_by(is_admin=True).all()
|
||||
|
||||
@classmethod
|
||||
def get_active_users(cls, days=30):
|
||||
"""取得活躍使用者(指定天數內有登入)"""
|
||||
cutoff_date = datetime.utcnow() - timedelta(days=days)
|
||||
return cls.query.filter(cls.last_login >= cutoff_date).all()
|
Reference in New Issue
Block a user