c6206787da
项目结构: - backend/ Python FastAPI 后端 - uni-app/ uni-app跨端前端 - docs/ 设计文档 - docker-compose.yml Docker编排 - nginx/scripts/systemd 运维配置 已完成功能: - 用户认证 (JWT) - 智能翻译 + 回复建议 - 营销素材生成 - 客户管理 + 沉默检测 - 报价单管理 - 产品库管理 - 汇率换算 - 推送通知 (uni-push) - WhatsApp Webhook框架 - Celery定时任务
119 lines
3.9 KiB
Python
119 lines
3.9 KiB
Python
from fastapi import Request
|
|
from starlette.middleware.base import BaseHTTPMiddleware
|
|
from app.config import settings
|
|
from app.core.security import decode_token
|
|
import redis.asyncio as aioredis
|
|
import logging
|
|
from datetime import datetime
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
def get_user_tier_from_token(request: Request) -> str:
|
|
auth = request.headers.get("Authorization", "")
|
|
if not auth.startswith("Bearer "):
|
|
return "anonymous"
|
|
payload = decode_token(auth[7:])
|
|
if not payload:
|
|
return "anonymous"
|
|
request.state.user_id = payload.get("sub")
|
|
request.state.user_tier = payload.get("tier", "free")
|
|
return request.state.user_tier
|
|
|
|
|
|
class TierMiddleware(BaseHTTPMiddleware):
|
|
async def dispatch(self, request: Request, call_next):
|
|
if request.url.path.startswith("/api/v1"):
|
|
tier = get_user_tier_from_token(request)
|
|
tier_config = {
|
|
"free": {
|
|
"max_products": settings.FREE_MAX_PRODUCTS,
|
|
"max_customers": settings.FREE_MAX_CUSTOMERS,
|
|
},
|
|
"pro": {
|
|
"max_products": settings.PRO_MAX_PRODUCTS,
|
|
"max_customers": settings.PRO_MAX_CUSTOMERS,
|
|
},
|
|
"enterprise": {
|
|
"max_products": 9999,
|
|
"max_customers": 99999,
|
|
},
|
|
}
|
|
request.state.tier_config = tier_config.get(tier, tier_config["free"])
|
|
else:
|
|
request.state.user_id = None
|
|
request.state.user_tier = "anonymous"
|
|
request.state.tier_config = {}
|
|
|
|
response = await call_next(request)
|
|
return response
|
|
|
|
|
|
class QuotaMiddleware(BaseHTTPMiddleware):
|
|
async def dispatch(self, request: Request, call_next):
|
|
if not request.url.path.startswith("/api/v1"):
|
|
return await call_next(request)
|
|
|
|
if request.state.user_tier in ("anonymous",):
|
|
return await call_next(request)
|
|
|
|
user_id = request.state.user_id
|
|
tier = request.state.user_tier
|
|
|
|
if tier == "enterprise":
|
|
return await call_next(request)
|
|
|
|
path = request.url.path
|
|
method = request.method
|
|
|
|
if method == "GET":
|
|
return await call_next(request)
|
|
|
|
quota_map = {
|
|
"/api/v1/translate": {
|
|
"free": settings.FREE_DAILY_TRANSLATE_CHARS,
|
|
"pro": settings.PRO_DAILY_TRANSLATE_CHARS,
|
|
},
|
|
"/api/v1/translate/reply": {
|
|
"free": settings.FREE_DAILY_REPLIES,
|
|
"pro": settings.PRO_DAILY_REPLIES,
|
|
},
|
|
"/api/v1/marketing": {
|
|
"free": settings.FREE_DAILY_MARKETING,
|
|
"pro": settings.PRO_DAILY_MARKETING,
|
|
},
|
|
"/api/v1/quotations": {
|
|
"free": settings.FREE_DAILY_QUOTATIONS,
|
|
"pro": settings.PRO_DAILY_QUOTATIONS,
|
|
},
|
|
}
|
|
|
|
matched_key = None
|
|
for prefix, limits in quota_map.items():
|
|
if path.startswith(prefix):
|
|
matched_key = prefix
|
|
break
|
|
|
|
if not matched_key:
|
|
return await call_next(request)
|
|
|
|
limit = quota_map[matched_key].get(tier)
|
|
if limit is None:
|
|
return await call_next(request)
|
|
|
|
try:
|
|
r = aioredis.from_url(settings.REDIS_URL)
|
|
key = f"quota:{user_id}:{matched_key}:{datetime.utcnow().strftime('%Y%m%d')}"
|
|
current = await r.incr(key)
|
|
await r.expire(key, 86400)
|
|
if current > limit:
|
|
from app.core.exceptions import QuotaExceededError
|
|
raise QuotaExceededError(matched_key)
|
|
request.state.quota_remaining = limit - current
|
|
except QuotaExceededError:
|
|
raise
|
|
except Exception as e:
|
|
logger.warning(f"Quota check failed: {e}")
|
|
|
|
return await call_next(request)
|