435 lines
15 KiB
Python
435 lines
15 KiB
Python
"""
|
||
数据库清理服务 - 清理过期的验证码和会话
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
from datetime import timedelta
|
||
|
||
from app.database.auth import OAuthToken
|
||
from app.database.verification import EmailVerification, LoginSession, TrustedDevice
|
||
from app.log import logger
|
||
from app.utils import utcnow
|
||
|
||
from sqlmodel import col, func, select
|
||
from sqlmodel.ext.asyncio.session import AsyncSession
|
||
|
||
|
||
class DatabaseCleanupService:
|
||
"""数据库清理服务"""
|
||
|
||
@staticmethod
|
||
async def cleanup_expired_verification_codes(db: AsyncSession) -> int:
|
||
"""
|
||
清理过期的邮件验证码
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
|
||
Returns:
|
||
int: 清理的记录数
|
||
"""
|
||
try:
|
||
# 查找过期的验证码记录
|
||
current_time = utcnow()
|
||
|
||
stmt = select(EmailVerification).where(EmailVerification.expires_at < current_time)
|
||
result = await db.exec(stmt)
|
||
expired_codes = result.all()
|
||
|
||
# 删除过期的记录
|
||
deleted_count = 0
|
||
for code in expired_codes:
|
||
await db.delete(code)
|
||
deleted_count += 1
|
||
|
||
await db.commit()
|
||
|
||
if deleted_count > 0:
|
||
logger.debug(f"[Cleanup Service] Cleaned up {deleted_count} expired email verification codes")
|
||
|
||
return deleted_count
|
||
|
||
except Exception as e:
|
||
await db.rollback()
|
||
logger.error(f"[Cleanup Service] Error cleaning expired verification codes: {e!s}")
|
||
return 0
|
||
|
||
@staticmethod
|
||
async def cleanup_expired_login_sessions(db: AsyncSession) -> int:
|
||
"""
|
||
清理过期的登录会话
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
|
||
Returns:
|
||
int: 清理的记录数
|
||
"""
|
||
try:
|
||
# 查找过期的登录会话记录
|
||
current_time = utcnow()
|
||
|
||
stmt = select(LoginSession).where(
|
||
LoginSession.expires_at < current_time, col(LoginSession.is_verified).is_(False)
|
||
)
|
||
result = await db.exec(stmt)
|
||
expired_sessions = result.all()
|
||
|
||
# 删除过期的记录
|
||
deleted_count = 0
|
||
for session in expired_sessions:
|
||
await db.delete(session)
|
||
deleted_count += 1
|
||
|
||
await db.commit()
|
||
|
||
if deleted_count > 0:
|
||
logger.debug(f"[Cleanup Service] Cleaned up {deleted_count} expired login sessions")
|
||
|
||
return deleted_count
|
||
|
||
except Exception as e:
|
||
await db.rollback()
|
||
logger.error(f"[Cleanup Service] Error cleaning expired login sessions: {e!s}")
|
||
return 0
|
||
|
||
@staticmethod
|
||
async def cleanup_old_used_verification_codes(db: AsyncSession, days_old: int = 7) -> int:
|
||
"""
|
||
清理旧的已使用验证码记录
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
days_old: 清理多少天前的已使用记录,默认7天
|
||
|
||
Returns:
|
||
int: 清理的记录数
|
||
"""
|
||
try:
|
||
# 查找指定天数前的已使用验证码记录
|
||
cutoff_time = utcnow() - timedelta(days=days_old)
|
||
|
||
stmt = select(EmailVerification).where(col(EmailVerification.is_used).is_(True))
|
||
result = await db.exec(stmt)
|
||
all_used_codes = result.all()
|
||
|
||
# 筛选出过期的记录
|
||
old_used_codes = [code for code in all_used_codes if code.used_at and code.used_at < cutoff_time]
|
||
|
||
# 删除旧的已使用记录
|
||
deleted_count = 0
|
||
for code in old_used_codes:
|
||
await db.delete(code)
|
||
deleted_count += 1
|
||
|
||
await db.commit()
|
||
|
||
if deleted_count > 0:
|
||
logger.debug(
|
||
f"[Cleanup Service] Cleaned up {deleted_count} used verification codes older than {days_old} days"
|
||
)
|
||
|
||
return deleted_count
|
||
|
||
except Exception as e:
|
||
await db.rollback()
|
||
logger.error(f"[Cleanup Service] Error cleaning old used verification codes: {e!s}")
|
||
return 0
|
||
|
||
@staticmethod
|
||
async def cleanup_unverified_login_sessions(db: AsyncSession, hours_old: int = 1) -> int:
|
||
"""
|
||
清理指定小时前创建但仍未验证的登录会话
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
hours_old: 清理多少小时前创建但仍未验证的会话,默认1小时
|
||
|
||
Returns:
|
||
int: 清理的记录数
|
||
"""
|
||
try:
|
||
# 计算截止时间
|
||
cutoff_time = utcnow() - timedelta(hours=hours_old)
|
||
|
||
# 查找指定时间前创建且仍未验证的会话记录
|
||
stmt = select(LoginSession).where(
|
||
col(LoginSession.is_verified).is_(False), LoginSession.created_at < cutoff_time
|
||
)
|
||
result = await db.exec(stmt)
|
||
unverified_sessions = result.all()
|
||
|
||
# 删除未验证的会话记录
|
||
deleted_count = 0
|
||
for session in unverified_sessions:
|
||
await db.delete(session)
|
||
deleted_count += 1
|
||
|
||
await db.commit()
|
||
|
||
if deleted_count > 0:
|
||
logger.debug(
|
||
f"[Cleanup Service] Cleaned up {deleted_count} unverified "
|
||
f"login sessions older than {hours_old} hour(s)"
|
||
)
|
||
|
||
return deleted_count
|
||
|
||
except Exception as e:
|
||
await db.rollback()
|
||
logger.error(f"[Cleanup Service] Error cleaning unverified login sessions: {e!s}")
|
||
return 0
|
||
|
||
@staticmethod
|
||
async def cleanup_outdated_verified_sessions(db: AsyncSession) -> int:
|
||
"""
|
||
清理过期会话记录
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
|
||
Returns:
|
||
int: 清理的记录数
|
||
"""
|
||
try:
|
||
stmt = select(LoginSession).where(
|
||
col(LoginSession.is_verified).is_(True), col(LoginSession.token_id).is_(None)
|
||
)
|
||
result = await db.exec(stmt)
|
||
# 删除旧的已验证记录
|
||
deleted_count = 0
|
||
for session in result.all():
|
||
await db.delete(session)
|
||
deleted_count += 1
|
||
|
||
await db.commit()
|
||
|
||
if deleted_count > 0:
|
||
logger.debug(f"[Cleanup Service] Cleaned up {deleted_count} outdated verified sessions")
|
||
|
||
return deleted_count
|
||
|
||
except Exception as e:
|
||
await db.rollback()
|
||
logger.error(f"[Cleanup Service] Error cleaning outdated verified sessions: {e!s}")
|
||
return 0
|
||
|
||
@staticmethod
|
||
async def cleanup_outdated_trusted_devices(db: AsyncSession) -> int:
|
||
"""
|
||
清理过期的受信任设备记录
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
|
||
Returns:
|
||
int: 清理的记录数
|
||
"""
|
||
try:
|
||
# 查找过期的受信任设备记录
|
||
current_time = utcnow()
|
||
|
||
stmt = select(TrustedDevice).where(TrustedDevice.expires_at < current_time)
|
||
result = await db.exec(stmt)
|
||
expired_devices = result.all()
|
||
|
||
# 删除过期的记录
|
||
deleted_count = 0
|
||
for device in expired_devices:
|
||
await db.delete(device)
|
||
deleted_count += 1
|
||
|
||
await db.commit()
|
||
|
||
if deleted_count > 0:
|
||
logger.debug(f"[Cleanup Service] Cleaned up {deleted_count} expired trusted devices")
|
||
|
||
return deleted_count
|
||
|
||
except Exception as e:
|
||
await db.rollback()
|
||
logger.error(f"[Cleanup Service] Error cleaning expired trusted devices: {e!s}")
|
||
return 0
|
||
|
||
@staticmethod
|
||
async def cleanup_outdated_tokens(db: AsyncSession) -> int:
|
||
"""
|
||
清理过期的 OAuth 令牌
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
|
||
Returns:
|
||
int: 清理的记录数
|
||
"""
|
||
try:
|
||
current_time = utcnow()
|
||
|
||
stmt = select(OAuthToken).where(OAuthToken.refresh_token_expires_at < current_time)
|
||
result = await db.exec(stmt)
|
||
expired_tokens = result.all()
|
||
|
||
deleted_count = 0
|
||
for token in expired_tokens:
|
||
await db.delete(token)
|
||
deleted_count += 1
|
||
|
||
await db.commit()
|
||
|
||
if deleted_count > 0:
|
||
logger.debug(f"[Cleanup Service] Cleaned up {deleted_count} expired OAuth tokens")
|
||
|
||
return deleted_count
|
||
|
||
except Exception as e:
|
||
await db.rollback()
|
||
logger.error(f"[Cleanup Service] Error cleaning expired OAuth tokens: {e!s}")
|
||
return 0
|
||
|
||
@staticmethod
|
||
async def run_full_cleanup(db: AsyncSession) -> dict[str, int]:
|
||
"""
|
||
运行完整的清理流程
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
|
||
Returns:
|
||
dict: 各项清理的结果统计
|
||
"""
|
||
results = {}
|
||
|
||
# 清理过期的验证码
|
||
results["expired_verification_codes"] = await DatabaseCleanupService.cleanup_expired_verification_codes(db)
|
||
|
||
# 清理过期的登录会话
|
||
results["expired_login_sessions"] = await DatabaseCleanupService.cleanup_expired_login_sessions(db)
|
||
|
||
# 清理1小时前未验证的登录会话
|
||
results["unverified_login_sessions"] = await DatabaseCleanupService.cleanup_unverified_login_sessions(db, 1)
|
||
|
||
# 清理7天前的已使用验证码
|
||
results["old_used_verification_codes"] = await DatabaseCleanupService.cleanup_old_used_verification_codes(db, 7)
|
||
|
||
# 清理过期的受信任设备
|
||
results["outdated_trusted_devices"] = await DatabaseCleanupService.cleanup_outdated_trusted_devices(db)
|
||
|
||
# 清理过期的 OAuth 令牌
|
||
results["outdated_oauth_tokens"] = await DatabaseCleanupService.cleanup_outdated_tokens(db)
|
||
|
||
# 清理过期(token 过期)的已验证会话
|
||
results["outdated_verified_sessions"] = await DatabaseCleanupService.cleanup_outdated_verified_sessions(db)
|
||
|
||
total_cleaned = sum(results.values())
|
||
if total_cleaned > 0:
|
||
logger.debug(
|
||
f"[Cleanup Service] Full cleanup completed, total cleaned: {total_cleaned} records - {results}"
|
||
)
|
||
|
||
return results
|
||
|
||
@staticmethod
|
||
async def get_cleanup_statistics(db: AsyncSession) -> dict[str, int]:
|
||
"""
|
||
获取清理统计信息
|
||
|
||
Args:
|
||
db: 数据库会话
|
||
|
||
Returns:
|
||
dict: 统计信息
|
||
"""
|
||
try:
|
||
current_time = utcnow()
|
||
cutoff_1_hour = current_time - timedelta(hours=1)
|
||
cutoff_7_days = current_time - timedelta(days=7)
|
||
cutoff_30_days = current_time - timedelta(days=30)
|
||
|
||
# 统计过期的验证码数量
|
||
expired_codes_stmt = (
|
||
select(func.count()).select_from(EmailVerification).where(EmailVerification.expires_at < current_time)
|
||
)
|
||
expired_codes_result = await db.exec(expired_codes_stmt)
|
||
expired_codes_count = expired_codes_result.one()
|
||
|
||
# 统计过期的登录会话数量
|
||
expired_sessions_stmt = (
|
||
select(func.count()).select_from(LoginSession).where(LoginSession.expires_at < current_time)
|
||
)
|
||
expired_sessions_result = await db.exec(expired_sessions_stmt)
|
||
expired_sessions_count = expired_sessions_result.one()
|
||
|
||
# 统计1小时前未验证的登录会话数量
|
||
unverified_sessions_stmt = (
|
||
select(func.count())
|
||
.select_from(LoginSession)
|
||
.where(col(LoginSession.is_verified).is_(False), LoginSession.created_at < cutoff_1_hour)
|
||
)
|
||
unverified_sessions_result = await db.exec(unverified_sessions_stmt)
|
||
unverified_sessions_count = unverified_sessions_result.one()
|
||
|
||
# 统计7天前的已使用验证码数量
|
||
old_used_codes_stmt = select(EmailVerification).where(col(EmailVerification.is_used).is_(True))
|
||
old_used_codes_result = await db.exec(old_used_codes_stmt)
|
||
all_used_codes = old_used_codes_result.all()
|
||
old_used_codes_count = len(
|
||
[code for code in all_used_codes if code.used_at and code.used_at < cutoff_7_days]
|
||
)
|
||
|
||
# 统计30天前的已验证会话数量
|
||
outdated_verified_sessions_stmt = select(LoginSession).where(col(LoginSession.is_verified).is_(True))
|
||
outdated_verified_sessions_result = await db.exec(outdated_verified_sessions_stmt)
|
||
all_verified_sessions = outdated_verified_sessions_result.all()
|
||
outdated_verified_sessions_count = len(
|
||
[
|
||
session
|
||
for session in all_verified_sessions
|
||
if session.verified_at and session.verified_at < cutoff_30_days
|
||
]
|
||
)
|
||
|
||
# 统计过期的 OAuth 令牌数量
|
||
outdated_tokens_stmt = (
|
||
select(func.count()).select_from(OAuthToken).where(OAuthToken.refresh_token_expires_at < current_time)
|
||
)
|
||
outdated_tokens_result = await db.exec(outdated_tokens_stmt)
|
||
outdated_tokens_count = outdated_tokens_result.one()
|
||
|
||
# 统计过期的受信任设备数量
|
||
outdated_devices_stmt = (
|
||
select(func.count()).select_from(TrustedDevice).where(TrustedDevice.expires_at < current_time)
|
||
)
|
||
outdated_devices_result = await db.exec(outdated_devices_stmt)
|
||
outdated_devices_count = outdated_devices_result.one()
|
||
|
||
return {
|
||
"expired_verification_codes": expired_codes_count,
|
||
"expired_login_sessions": expired_sessions_count,
|
||
"unverified_login_sessions": unverified_sessions_count,
|
||
"old_used_verification_codes": old_used_codes_count,
|
||
"outdated_verified_sessions": outdated_verified_sessions_count,
|
||
"outdated_oauth_tokens": outdated_tokens_count,
|
||
"outdated_trusted_devices": outdated_devices_count,
|
||
"total_cleanable": expired_codes_count
|
||
+ expired_sessions_count
|
||
+ unverified_sessions_count
|
||
+ old_used_codes_count
|
||
+ outdated_verified_sessions_count
|
||
+ outdated_tokens_count
|
||
+ outdated_devices_count,
|
||
}
|
||
|
||
except Exception as e:
|
||
logger.error(f"[Cleanup Service] Error getting cleanup statistics: {e!s}")
|
||
return {
|
||
"expired_verification_codes": 0,
|
||
"expired_login_sessions": 0,
|
||
"unverified_login_sessions": 0,
|
||
"old_used_verification_codes": 0,
|
||
"outdated_verified_sessions": 0,
|
||
"outdated_oauth_tokens": 0,
|
||
"outdated_trusted_devices": 0,
|
||
"total_cleanable": 0,
|
||
}
|