تمرين المرحلة الثانية الشامل — تنفيذ حلقة وظائف PriceTracker الأساسية
المرحلة الثانية مثل التجميع النهائي للسيارة—المحرك (قاعدة البيانات)، وعجلة القيادة (التوجيه)، وأحزمة الأمان (المصادقة)، ولوحة القيادة (البرمجيات الوسيطة) تم بناؤها جميعًا، والآن يتم تجميعها في سيارة كاملة جاهزة للانطلاق على الطريق.
1. ما ستتعلمه
- تحسين سلسلة DI ثلاثية المستويات:
get_db→get_current_user→require_subscription("pro") - حماية JWT الشاملة: سياسات مجموعة التوجيه لنقاط النهاية العامة مقابل نقاط النهاية المصادق عليها
- تكوين برمجية CORS الوسيطة: السماح لنطاق Bob الواجهي بالوصول إلى الموارد عبر النطاقات
- سير عمل الاختبار الشامل لعمليات CRUD الكاملة والتقسيم والتحميل المسبق للبيانات المرتبطة
- التحقق من سيناريو Alice: يمكن للمستخدمين Pro استيراد ملايين نقاط بيانات الأسعار بشكل مجمّع، بينما يقتصر المستخدمون المجانيون على 1000 سجل
2. القصة الحقيقية لـ Alice
(1) نقطة الألم: الوظائف مبعثرة ولا يمكن دمجها للاختبار
نفذت Alice البرمجيات الوسيطة وحقن الاعتماد وقاعدة البيانات و CRUD ومصادقة JWT بشكل منفصل، ولكن كل ميزة تم اختبارها بشكل مستقل. عندما حاولت دمج جميع هذه الميزات، اكتشفت أن ترتيب تنفيذ برمجية CORS الوسيطة ومصادقة JWT يتعارض، وأن جلسة DB تم إنشاؤها مرتين في سلسلة DI، وأن منطق تحديد المعدل وفحوصات الصلاحيات للمستخدمين Pro يتجاوز بعضها البعض.
(2) نهج منظم ومتكامل
تمرين المرحلة الثانية الشامل يدمج جميع الوحدات بالترتيب الهرمي الصحيح: CORS (الطبقة الخارجية) → تحديد المعدل → المصادقة → منطق الأعمال → قاعدة البيانات (الطبقة الداخلية)، مع مسؤوليات محددة بوضوح لكل طبقة تعمل بشكل مستقل عن الأخرى.
(3) العائد
خدمة PriceTracker الأساسية المدمجة تعمل بالكامل: نجحت الواجهة الأمامية لـ Bob في الوصول إلى الموارد عبر النطاقات، وجميع نقاط النهاية المحمية تتطلب رمز JWT، وتم استيراد المستخدمين Pro بشكل مجمّع بدون مشاكل، والمستخدمون المجانيون محدودون بالمعدل بشكل صحيح.
3. نظرة عامة على بنية المرحلة الثانية
(1) تدفق الطلب الكامل
flowchart TD
Client[Bob الواجهة الأمامية] --> CORS[برمجية CORS الوسيطة]
CORS --> RateLimit[برمجية تحديد المعدل الوسيطة]
RateLimit --> Router[FastAPI Router]
Router --> AuthDI{هل المصادقة مطلوبة؟}
AuthDI -->|نعم| GetDB[اعتماد get_db]
AuthDI -->|لا| Handler[المعالج]
GetDB --> GetUser[get_current_user]
GetUser --> CheckSub[require_subscription]
CheckSub --> Handler
Handler --> Repo[طبقة Repository]
Repo --> DB[(PostgreSQL)]
RateLimit -.->|429| Reject[محدود المعدل]
GetUser -.->|401| Unauth[غير مصادق]
CheckSub -.->|403| Forbid[محظور]
(2) سياسة مجموعة التوجيه
| مجموعة التوجيه | البادئة | المصادقة | الوصف |
|---|---|---|---|
| عامة | / |
بدون | health، docs |
| api_v1 | /api/v1 |
JWT مطلوب | جميع نقاط نهاية الأعمال |
| admin | /api/v1/admin |
JWT + مسؤول | نقاط نهاية الإدارة |
4. دمج الكود الكامل
(1) نقاط دخول التطبيق وتكوين البرمجيات الوسيطة
(1) ▶ مثال: تكوين كامل لـ main.py
from fastapi import FastAPI
from starlette.middleware.cors import CORSMiddleware
from app.api.routes import products, prices, auth
from app.core.config import settings
app = FastAPI(
title=settings.app_name,
version="1.0.0",
description="PriceTracker SaaS API - خدمة تتبع أسعار التجارة الإلكترونية",
)
# ترتيب البرمجيات الوسيطة: آخر تسجيل = الأعمق (ينفذ أولًا عند الطلب)
# يجب أن تكون CORS في الطبقة الخارجية (تُسجل أخيرًا)
app.add_middleware(
CORSMiddleware,
allow_origins=settings.allowed_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# تضمين الموجهات - العامة أولًا، ثم المحمية
app.include_router(auth.router, tags=["auth"])
app.include_router(
products.router,
prefix="/api/v1",
dependencies=[Depends(get_current_user)],
tags=["products"],
)
app.include_router(
prices.router,
prefix="/api/v1",
dependencies=[Depends(get_current_user)],
tags=["prices"],
)
@app.get("/health")
async def health_check():
return {"status": "healthy", "service": "pricetracker"}
الناتج:
# تم تعريف الدالة بنجاح
(2) تنفيذ كامل لسلسلة DI ثلاثية الطبقات
(2) ▶ مثال: app/core/deps.py
from fastapi import Depends, HTTPException, Header
from fastapi.security import OAuth2PasswordBearer
from sqlalchemy.ext.asyncio import AsyncSession
from jose import jwt, JWTError
from app.db import async_session
from app.core.config import settings
from app.core.security import verify_password
from app.models import User
from sqlalchemy import select
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/login")
# المستوى 1: جلسة قاعدة البيانات
async def get_db():
async with async_session() as session:
try:
yield session
except Exception:
await session.rollback()
raise
# المستوى 2: المستخدم الحالي
async def get_current_user(
token: str = Depends(oauth2_scheme),
db: AsyncSession = Depends(get_db),
):
credentials_exception = HTTPException(
status_code=401,
detail="Could not validate credentials",
headers={"WWW-Authenticate": "Bearer"},
)
try:
payload = jwt.decode(
token, settings.secret_key, algorithms=[settings.algorithm]
)
username = payload.get("sub")
if username is None:
raise credentials_exception
except JWTError:
raise credentials_exception
stmt = select(User).where(User.email == username)
result = await db.execute(stmt)
user = result.scalar_one_or_none()
if user is None:
raise credentials_exception
return user
# المستوى 3: فحص الاشتراك
def require_subscription(min_level: str = "free"):
LEVELS = {"free": 0, "pro": 1, "enterprise": 2}
async def check(user: User = Depends(get_current_user)):
if LEVELS.get(user.subscription, 0) < LEVELS.get(min_level, 0):
raise HTTPException(
status_code=403,
detail=f"Requires {min_level} plan. Current: {user.subscription}",
)
return user
return check
الناتج:
# تم تعريف الدالة بنجاح
(3) عملية التحقق من الصلاحيات
flowchart LR
Request[طلب مع رمز] --> ParseToken[تحليل رمز JWT]
ParseToken --> ValidToken{الرمز صالح؟}
ValidToken -->|لا| Return401[إرجاع 401]
ValidToken -->|نعم| QueryUser[الاستعلام عن المستخدم من DB]
QueryUser --> UserExists{تم العثور على المستخدم؟}
UserExists -->|لا| Return401
UserExists -->|نعم| CheckSub{مستوى الاشتراك؟}
CheckSub -->|مجاني| AllowBasic[السماح بنقاط النهاية الأساسية]
CheckSub -->|Pro| AllowPro[السماح بنقاط نهاية Pro]
CheckSub -->|Enterprise| AllowAll[السماح بجميع نقاط النهاية]
(3) ▶ مثال: توجيه المنتجات المحمي
from fastapi import APIRouter, Depends, HTTPException, Path, Query
from sqlalchemy.ext.asyncio import AsyncSession
from typing import Optional
from app.core.deps import get_db, get_current_user, require_subscription
from app.schemas import ProductCreate, ProductResponse, ProductUpdate
from app.services import ProductService
router = APIRouter()
@router.get("/products", response_model=list[ProductResponse])
async def list_products(
category: Optional[str] = Query(None),
skip: int = Query(0, ge=0),
limit: int = Query(20, ge=1, le=100),
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user),
):
service = ProductService(db)
return await service.list_products(category=category, skip=skip, limit=limit)
@router.post("/products", response_model=ProductResponse, status_code=201)
async def create_product(
product: ProductCreate,
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user),
):
service = ProductService(db)
return await service.create_product(product, user_id=user.id)
@router.get("/products/{product_id}", response_model=ProductResponse)
async def get_product(
product_id: int = Path(gt=0),
db: AsyncSession = Depends(get_db),
user=Depends(get_current_user),
):
service = ProductService(db)
product = await service.get_product(product_id)
if not product:
raise HTTPException(status_code=404, detail="Product not found")
return product
الناتج:
# تم تعريف الدالة بنجاح
(4) ▶ مثال: استيراد مجمّع لنقاط النهاية (بما في ذلك حدود الاشتراك)
@router.post("/products/{product_id}/prices/bulk")
async def bulk_import_prices(
product_id: int = Path(gt=0),
prices: list[PriceCreate] = ...,
db: AsyncSession = Depends(get_db),
user=Depends(require_subscription("free")), # حتى المستخدمون المجانيون يمكنهم الاستيراد
):
# خطة مجانية: حد أقصى 1000 سعر لكل استيراد
limits = {"free": 1000, "pro": 100000, "enterprise": 1000000}
max_import = limits.get(user.subscription, 1000)
if len(prices) > max_import:
raise HTTPException(
status_code=403,
detail=f"Import limit: {max_import} for {user.subscription} plan. Upgrade at /pricing",
)
service = PriceService(db)
return await service.bulk_import_prices(product_id, prices)
الناتج:
# تم تعريف الدالة بنجاح
❓ أسئلة شائعة
Depends(get_current_user) لمجموعة التوجيه و Depends(get_current_user) لنقطة النهاية يتشاركان نفس نتيجة التنفيذ.get_db عدة مرات في سلسلة DI، فهل سيكون هناك اتصالات متعددة؟get_db مرة واحدة فقط لكل طلب، وجميع الاعتمادات تتشارك نفس AsyncSession.User.subscription للمحاكاة؛ في بيئة الإنتاج، يتم تحديثه عبر رد الاتصال بالدفع.get_db بـ TestClient + dependency_overrides واختبر خطوة بخطوة: بدون رمز → 401، رمز مجاني → 200 (لكن مع قيود)، رمز Pro → صلاحيات كاملة.📖 ملخص
- المرحلة الثانية تدمج خمس وحدات رئيسية—البرمجيات الوسيطة و DI وقاعدة البيانات و CRUD و JWT—في خدمة كاملة الوظيفة
- سلسلة DI ثلاثية المستويات (get_db → get_current_user → require_subscription) تدير المصادقة والتفويض بشكل مركزي
- سياسات مجموعة التوجيه: "عامة" لا تتطلب مصادقة؛ "api_v1" محمية بـ JWT؛ "admin" تتطلب JWT وصلاحيات مسؤول
- يجب تسجيل CORS في الطبقة الخارجية لضمان أن جميع الاستجابات (بما في ذلك استجابات الخطأ) تتضمن رؤوس CORS
- مستويات الاشتراك تُنفذ عبر سلسلة DI: المجاني محدود بـ 1000 استيراد، و Pro بـ 100,000، و Enterprise بـ 1,000,000
📝 تمارين
- تمرين أساسي (الصعوبة ⭐): استبدل تنفيذ CRUD في الذاكرة من المرحلة الأولى بتنفيذ SQLAlchemy غير المتزامن، مع التأكد من أن جميع نقاط النهاية تحقن جلسة قاعدة البيانات عبر
Depends(get_db). تلميح: نمط Repository +AsyncSession - تمرين متقدم (الصعوبة ⭐⭐): نفذ مجموعات التوجيه—المجموعة "عامة" (/health، /auth/login) لا تتطلب مصادقة، بينما جميع نقاط نهاية الأعمال تحت "api/v1" محمية بـ JWT. استخدم Swagger UI لاختبار تسجيل الدخول والوصول إلى نقاط النهاية المحمية. تلميح:
APIRouter(dependencies=[Depends(get_current_user)]) - تحدي (الصعوبة ⭐⭐⭐): نفذ سلسلة DI ثلاثية الطبقات الكاملة + استيراد مجمّع لنقاط النهاية: يمكن للمستخدمين Pro استيراد 100,000 سجل سعر؛ سيتلقى المستخدمون المجانيون خطأ 403 إذا تجاوزوا 1000 سجل. أيضًا، اعرض عملية التسجيل → تسجيل الدخول → الاستيراد بالكامل في Swagger UI. تلميح:
require_subscription("free")+ قاموسlimits
---|



