people-reading/backend/app/services/quota_service.py
2026-05-12 20:50:15 +08:00

64 lines
2.2 KiB
Python

from datetime import datetime, time, timedelta
from zoneinfo import ZoneInfo
from fastapi import HTTPException, status
from sqlalchemy import select
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.config import settings
from app.models.daily_usage import DailyUsage
SHANGHAI_TZ = ZoneInfo("Asia/Shanghai")
class QuotaService:
def today(self):
return datetime.now(SHANGHAI_TZ).date()
def reset_at(self) -> datetime:
tomorrow = self.today() + timedelta(days=1)
return datetime.combine(tomorrow, time.min, tzinfo=SHANGHAI_TZ)
async def get_quota(self, db: AsyncSession, user_id: str) -> dict:
usage = await self._get_usage(db, user_id)
used = usage.count if usage else 0
return self._quota_payload(used)
async def consume(self, db: AsyncSession, user_id: str) -> dict:
usage = await self._get_usage(db, user_id)
if usage is None:
usage = DailyUsage(user_id=user_id, usage_date=self.today(), count=0)
db.add(usage)
await db.flush()
if usage.count >= settings.daily_reading_limit:
quota = self._quota_payload(usage.count)
raise HTTPException(
status_code=status.HTTP_429_TOO_MANY_REQUESTS,
detail=f"今日 {settings.daily_reading_limit} 次解读机会已用完,请明天 0 点后再来。",
headers={
"X-RateLimit-Limit": str(quota["limit"]),
"X-RateLimit-Remaining": str(quota["remaining"]),
"X-RateLimit-Reset": quota["reset_at"].isoformat(),
},
)
usage.count += 1
await db.flush()
return self._quota_payload(usage.count)
async def _get_usage(self, db: AsyncSession, user_id: str) -> DailyUsage | None:
result = await db.execute(
select(DailyUsage).where(DailyUsage.user_id == user_id, DailyUsage.usage_date == self.today())
)
return result.scalar_one_or_none()
def _quota_payload(self, used: int) -> dict:
limit = settings.daily_reading_limit
return {
"limit": limit,
"used": used,
"remaining": max(0, limit - used),
"reset_at": self.reset_at(),
}