feat: multi-tenant workspace isolation, RBAC, sidebar nav, billing, and Android enhancements
- Backend: workspace_id isolation for 14 model tables + safe migration/backfill - Backend: RBAC system with 4 roles and 23 permissions, seeded on startup - Backend: workspace admin endpoints (list/manage all workspaces) - Backend: admin user management API (CRUD, reset password) - Backend: billing API with subscription plans, usage tracking, rate limiting - Backend: fix system_logs.py UNION query and wrong column references - Backend: WebSocket JWT auth and workspace enforcement - Frontend: sidebar navigation replacing top dropdown menu - Frontend: user management page (Users.vue) for admins - Frontend: enhanced Workspaces.vue with admin table view - Frontend: workspace RBAC computed properties in user store - Android: agent marketplace, billing/subscription UI, onboarding wizard - Android: phone login, analytics tracker, crash handler, network diagnostics - Android: splash screen, encrypted token storage, app update enhancements - Docs: multi-tenant RBAC guide with 8 sections and role-permission matrix Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
119
backend/app/core/usage_middleware.py
Normal file
119
backend/app/core/usage_middleware.py
Normal file
@@ -0,0 +1,119 @@
|
||||
"""
|
||||
用量追踪中间件:每日对话次数限制与强制执行
|
||||
"""
|
||||
import logging
|
||||
from datetime import date
|
||||
from fastapi import Request
|
||||
from starlette.middleware.base import BaseHTTPMiddleware
|
||||
from starlette.responses import JSONResponse
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.core.database import SessionLocal
|
||||
from app.core.security import decode_access_token
|
||||
from app.models.user import User
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# 套餐每日限额
|
||||
TIER_DAILY_LIMITS = {
|
||||
"free": 50,
|
||||
"pro": 500,
|
||||
"enterprise": 0, # 0 = 无限
|
||||
}
|
||||
|
||||
# 需要计数的路径前缀
|
||||
COUNTED_PATH_PREFIXES = [
|
||||
"/api/v1/agent-chat/",
|
||||
]
|
||||
|
||||
# 不计数路径(导出、会话管理等非对话操作)
|
||||
EXCLUDED_PATH_PATTERNS = [
|
||||
"/sessions/",
|
||||
"/search-messages",
|
||||
"/export",
|
||||
]
|
||||
|
||||
|
||||
class UsageMiddleware(BaseHTTPMiddleware):
|
||||
"""用量追踪中间件:检查每日限额 → 放行 → 递增计数"""
|
||||
|
||||
async def dispatch(self, request: Request, call_next):
|
||||
path = request.url.path
|
||||
should_count = (
|
||||
request.method == "POST"
|
||||
and any(path.startswith(p) for p in COUNTED_PATH_PREFIXES)
|
||||
and not any(p in path for p in EXCLUDED_PATH_PATTERNS)
|
||||
)
|
||||
|
||||
if not should_count:
|
||||
return await call_next(request)
|
||||
|
||||
# 从 Authorization header 解码 user_id
|
||||
user_id = None
|
||||
auth_header = request.headers.get("Authorization", "")
|
||||
if auth_header.startswith("Bearer "):
|
||||
token = auth_header[7:]
|
||||
try:
|
||||
payload = decode_access_token(token)
|
||||
user_id = payload.get("sub")
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if not user_id:
|
||||
return await call_next(request)
|
||||
|
||||
db: Session = SessionLocal()
|
||||
try:
|
||||
user = db.query(User).filter(User.id == user_id).first()
|
||||
if not user:
|
||||
return await call_next(request)
|
||||
|
||||
today = date.today()
|
||||
|
||||
# 日期重置
|
||||
if user.daily_usage_date and user.daily_usage_date < today:
|
||||
user.daily_usage_count = 0
|
||||
user.daily_usage_date = today
|
||||
if not user.daily_usage_date:
|
||||
user.daily_usage_date = today
|
||||
|
||||
tier = user.subscription_tier or "free"
|
||||
limit = TIER_DAILY_LIMITS.get(tier, 50)
|
||||
current = user.daily_usage_count or 0
|
||||
|
||||
# 检查是否已达限额
|
||||
if limit > 0 and current >= limit:
|
||||
db.close()
|
||||
logger.warning("用户 %s 已达每日限额 %s/%s,拒绝请求", user_id, current, limit)
|
||||
return JSONResponse(
|
||||
status_code=429,
|
||||
content={
|
||||
"detail": f"今日对话次数已用完({current}/{limit})。升级 Pro 版享受每日 {TIER_DAILY_LIMITS.get('pro', 500)} 次对话。",
|
||||
"daily_used": current,
|
||||
"daily_limit": limit,
|
||||
"upgrade_required": True,
|
||||
},
|
||||
headers={"X-Upgrade-Required": "true"},
|
||||
)
|
||||
|
||||
# 未达限额:先放行,再递增
|
||||
response = await call_next(request)
|
||||
|
||||
if response.status_code < 400:
|
||||
# 递增计数(需要重新查询,因为 request 可能耗时较长)
|
||||
db_user = db.query(User).filter(User.id == user_id).first()
|
||||
if db_user:
|
||||
db_user.daily_usage_count = (db_user.daily_usage_count or 0) + 1
|
||||
db.commit()
|
||||
|
||||
new_count = db_user.daily_usage_count
|
||||
if limit > 0 and new_count >= limit:
|
||||
logger.info("用户 %s 已达每日限额 %s/%s", user_id, new_count, limit)
|
||||
|
||||
return response
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
logger.error("用量追踪失败: %s", e)
|
||||
return await call_next(request)
|
||||
finally:
|
||||
db.close()
|
||||
Reference in New Issue
Block a user