Initial commit: TradeMate 外贸小助手 MVP
项目结构: - backend/ Python FastAPI 后端 - uni-app/ uni-app跨端前端 - docs/ 设计文档 - docker-compose.yml Docker编排 - nginx/scripts/systemd 运维配置 已完成功能: - 用户认证 (JWT) - 智能翻译 + 回复建议 - 营销素材生成 - 客户管理 + 沉默检测 - 报价单管理 - 产品库管理 - 汇率换算 - 推送通知 (uni-push) - WhatsApp Webhook框架 - Celery定时任务
This commit is contained in:
@@ -0,0 +1,118 @@
|
||||
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)
|
||||
Reference in New Issue
Block a user