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(), }