fastapi-patterns
1
总安装量
1
周安装量
#41221
全站排名
安装命令
npx skills add https://github.com/seikaikyo/dash-skills --skill fastapi-patterns
Agent 安装分布
amp
1
opencode
1
kimi-cli
1
codex
1
github-copilot
1
gemini-cli
1
Skill 文档
FastAPI + SQLModel å¾ç«¯éç¼è¦ç¯
é©ç¨å ´æ¯
- Render é¨ç½²ç API æå
- æé Neon PostgreSQL
- RESTful API è¨è¨
- MES/ERP å¾ç«¯æå
å°æ¡çµæ§
project/
âââ app/
â âââ __init__.py
â âââ main.py # FastAPI æç¨å
¥å£
â âââ config.py # ç°å¢è¨å®
â âââ database.py # è³æåº«é£ç·
â âââ dependencies.py # å
±ç¨ä¾è³´
â âââ models/ # SQLModel è³ææ¨¡å
â â âââ __init__.py
â â âââ base.py
â â âââ work_order.py
â â âââ user.py
â âââ schemas/ # Pydantic è«æ±/åæ schema
â â âââ __init__.py
â â âââ base.py
â â âââ work_order.py
â âââ routers/ # API è·¯ç±
â â âââ __init__.py
â â âââ work_orders.py
â â âââ users.py
â âââ services/ # æ¥åé輯
â â âââ __init__.py
â â âââ work_order_service.py
â âââ utils/ # å·¥å
·å½æ¸
â âââ __init__.py
â âââ audit.py
âââ requirements.txt
âââ render.yaml
âââ .env.example
æ ¸å¿è¨å®
config.py
from pydantic_settings import BaseSettings
from functools import lru_cache
class Settings(BaseSettings):
# è³æåº«
database_url: str
# API
api_prefix: str = "/api"
debug: bool = False
# èªè
jwt_secret: str
jwt_algorithm: str = "HS256"
jwt_expire_minutes: int = 60 * 24 # 24 å°æ
# CORS
cors_origins: list[str] = ["*"]
class Config:
env_file = ".env"
@lru_cache
def get_settings() -> Settings:
return Settings()
database.py
from sqlmodel import SQLModel, create_engine, Session
from app.config import get_settings
settings = get_settings()
# Neon PostgreSQL é£ç·
engine = create_engine(
settings.database_url,
echo=settings.debug,
pool_pre_ping=True, # é£ç·å¥åº·æª¢æ¥
pool_recycle=300, # 5 åéåæ¶é£ç·
)
def init_db():
"""å»ºç«ææè³æè¡¨"""
SQLModel.metadata.create_all(engine)
def get_session():
"""åå¾è³æåº« sessionï¼ä¾è³´æ³¨å
¥ç¨ï¼"""
with Session(engine) as session:
yield session
main.py
from fastapi import FastAPI
from fastapi.middleware.cors import CORSMiddleware
from contextlib import asynccontextmanager
from app.config import get_settings
from app.database import init_db
from app.routers import work_orders, users
settings = get_settings()
@asynccontextmanager
async def lifespan(app: FastAPI):
# ååæ
init_db()
yield
# ééæï¼æ¸
çè³æºï¼
app = FastAPI(
title="MES API",
version="1.0.0",
lifespan=lifespan,
)
# CORS
app.add_middleware(
CORSMiddleware,
allow_origins=settings.cors_origins,
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)
# è·¯ç±
app.include_router(work_orders.router, prefix=f"{settings.api_prefix}/work-orders", tags=["å·¥å®"])
app.include_router(users.router, prefix=f"{settings.api_prefix}/users", tags=["使ç¨è
"])
@app.get("/health")
def health_check():
"""å¥åº·æª¢æ¥ï¼Render ç¨ï¼"""
return {"status": "ok"}
è³ææ¨¡å (SQLModel)
models/base.py
from sqlmodel import SQLModel, Field
from datetime import datetime
from typing import Optional
import uuid
class BaseModel(SQLModel):
"""åºç¤æ¨¡å"""
id: str = Field(default_factory=lambda: str(uuid.uuid4()), primary_key=True)
created_at: datetime = Field(default_factory=datetime.utcnow)
updated_at: Optional[datetime] = Field(default=None)
class Config:
json_encoders = {
datetime: lambda v: v.isoformat()
}
models/work_order.py
from sqlmodel import Field, Relationship
from typing import Optional, List
from datetime import datetime
from enum import Enum
from app.models.base import BaseModel
class WorkOrderStatus(str, Enum):
PENDING = "pending"
IN_PROGRESS = "in_progress"
COMPLETED = "completed"
CANCELLED = "cancelled"
class WorkOrder(BaseModel, table=True):
"""å·¥å®è³æè¡¨"""
__tablename__ = "work_orders"
order_number: str = Field(unique=True, index=True)
customer_id: str = Field(foreign_key="customers.id", index=True)
product_model: str
quantity: int = Field(ge=1)
status: WorkOrderStatus = Field(default=WorkOrderStatus.PENDING, index=True)
priority: int = Field(default=5, ge=1, le=10)
due_date: Optional[datetime] = None
note: Optional[str] = None
# éè¯
dispatches: List["Dispatch"] = Relationship(back_populates="work_order")
è«æ±/åæ Schema
schemas/base.py
from pydantic import BaseModel
from typing import TypeVar, Generic, Optional, List
T = TypeVar("T")
class ApiResponse(BaseModel, Generic[T]):
"""çµ±ä¸åææ ¼å¼"""
success: bool
data: Optional[T] = None
error: Optional[dict] = None
message: Optional[str] = None
class PaginatedResponse(ApiResponse[List[T]], Generic[T]):
"""åé åæ"""
pagination: Optional[dict] = None
def success_response(data: T, message: str = "æå") -> ApiResponse[T]:
return ApiResponse(success=True, data=data, message=message)
def error_response(code: str, message: str) -> ApiResponse:
return ApiResponse(success=False, error={"code": code, "message": message})
schemas/work_order.py
from pydantic import BaseModel, Field
from typing import Optional
from datetime import datetime
from app.models.work_order import WorkOrderStatus
class WorkOrderCreate(BaseModel):
"""æ°å¢å·¥å®è«æ±"""
order_number: str = Field(..., min_length=1, max_length=50)
customer_id: str
product_model: str
quantity: int = Field(..., ge=1)
priority: int = Field(default=5, ge=1, le=10)
due_date: Optional[datetime] = None
note: Optional[str] = None
class WorkOrderUpdate(BaseModel):
"""æ´æ°å·¥å®è«æ±"""
product_model: Optional[str] = None
quantity: Optional[int] = Field(default=None, ge=1)
status: Optional[WorkOrderStatus] = None
priority: Optional[int] = Field(default=None, ge=1, le=10)
due_date: Optional[datetime] = None
note: Optional[str] = None
class WorkOrderResponse(BaseModel):
"""å·¥å®åæ"""
id: str
order_number: str
customer_id: str
product_model: str
quantity: int
status: WorkOrderStatus
priority: int
due_date: Optional[datetime]
note: Optional[str]
created_at: datetime
updated_at: Optional[datetime]
class Config:
from_attributes = True
è·¯ç±è¨è¨
routers/work_orders.py
from fastapi import APIRouter, Depends, HTTPException, Query
from sqlmodel import Session, select
from typing import List, Optional
from datetime import datetime
from app.database import get_session
from app.models.work_order import WorkOrder, WorkOrderStatus
from app.schemas.base import ApiResponse, PaginatedResponse, success_response, error_response
from app.schemas.work_order import WorkOrderCreate, WorkOrderUpdate, WorkOrderResponse
from app.dependencies import get_current_user
from app.utils.audit import log_audit
router = APIRouter()
@router.get("", response_model=PaginatedResponse[WorkOrderResponse])
def list_work_orders(
status: Optional[WorkOrderStatus] = None,
customer_id: Optional[str] = None,
page: int = Query(1, ge=1),
limit: int = Query(20, ge=1, le=100),
session: Session = Depends(get_session),
):
"""åå¾å·¥å®å表"""
query = select(WorkOrder)
# ç¯©é¸æ¢ä»¶
if status:
query = query.where(WorkOrder.status == status)
if customer_id:
query = query.where(WorkOrder.customer_id == customer_id)
# åé
total = session.exec(select(func.count()).select_from(query.subquery())).one()
offset = (page - 1) * limit
query = query.offset(offset).limit(limit).order_by(WorkOrder.created_at.desc())
items = session.exec(query).all()
return PaginatedResponse(
success=True,
data=[WorkOrderResponse.from_orm(item) for item in items],
pagination={"total": total, "page": page, "limit": limit}
)
@router.get("/{work_order_id}", response_model=ApiResponse[WorkOrderResponse])
def get_work_order(
work_order_id: str,
session: Session = Depends(get_session),
):
"""åå¾å®ä¸å·¥å®"""
work_order = session.get(WorkOrder, work_order_id)
if not work_order:
raise HTTPException(status_code=404, detail="å·¥å®ä¸åå¨")
return success_response(WorkOrderResponse.from_orm(work_order))
@router.post("", response_model=ApiResponse[WorkOrderResponse])
def create_work_order(
data: WorkOrderCreate,
session: Session = Depends(get_session),
current_user: dict = Depends(get_current_user),
):
"""æ°å¢å·¥å®"""
# 檢æ¥å·¥å®ç·¨èæ¯å¦éè¤
existing = session.exec(
select(WorkOrder).where(WorkOrder.order_number == data.order_number)
).first()
if existing:
raise HTTPException(status_code=400, detail="å·¥å®ç·¨èå·²åå¨")
work_order = WorkOrder(**data.model_dump())
session.add(work_order)
session.commit()
session.refresh(work_order)
# ç¨½æ ¸ç´é
log_audit(
session=session,
user_id=current_user["id"],
action="CREATE",
resource_type="work_order",
resource_id=work_order.id,
details=data.model_dump()
)
return success_response(WorkOrderResponse.from_orm(work_order), "å·¥å®å»ºç«æå")
@router.put("/{work_order_id}", response_model=ApiResponse[WorkOrderResponse])
def update_work_order(
work_order_id: str,
data: WorkOrderUpdate,
session: Session = Depends(get_session),
current_user: dict = Depends(get_current_user),
):
"""æ´æ°å·¥å®"""
work_order = session.get(WorkOrder, work_order_id)
if not work_order:
raise HTTPException(status_code=404, detail="å·¥å®ä¸åå¨")
# æ´æ°æ¬ä½
update_data = data.model_dump(exclude_unset=True)
for key, value in update_data.items():
setattr(work_order, key, value)
work_order.updated_at = datetime.utcnow()
session.add(work_order)
session.commit()
session.refresh(work_order)
# ç¨½æ ¸ç´é
log_audit(
session=session,
user_id=current_user["id"],
action="UPDATE",
resource_type="work_order",
resource_id=work_order.id,
details=update_data
)
return success_response(WorkOrderResponse.from_orm(work_order), "工宿´æ°æå")
@router.delete("/{work_order_id}", response_model=ApiResponse)
def delete_work_order(
work_order_id: str,
session: Session = Depends(get_session),
current_user: dict = Depends(get_current_user),
):
"""åªé¤å·¥å®"""
work_order = session.get(WorkOrder, work_order_id)
if not work_order:
raise HTTPException(status_code=404, detail="å·¥å®ä¸åå¨")
session.delete(work_order)
session.commit()
# ç¨½æ ¸ç´é
log_audit(
session=session,
user_id=current_user["id"],
action="DELETE",
resource_type="work_order",
resource_id=work_order_id,
)
return success_response(None, "å·¥å®åªé¤æå")
èªèææ¬
dependencies.py
from fastapi import Depends, HTTPException, status
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
import jwt
from datetime import datetime
from app.config import get_settings
settings = get_settings()
security = HTTPBearer()
def get_current_user(
credentials: HTTPAuthorizationCredentials = Depends(security)
) -> dict:
"""é©è JWT Token 並åå¾ç¶å使ç¨è
"""
token = credentials.credentials
try:
payload = jwt.decode(
token,
settings.jwt_secret,
algorithms=[settings.jwt_algorithm]
)
# 檢æ¥éæ
exp = payload.get("exp")
if exp and datetime.utcnow().timestamp() > exp:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token å·²éæ"
)
return payload
except jwt.InvalidTokenError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="ç¡æç Token"
)
def require_role(allowed_roles: list[str]):
"""è§è²æ¬é檢æ¥"""
def role_checker(current_user: dict = Depends(get_current_user)):
user_role = current_user.get("role", "")
if user_role not in allowed_roles:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="æ¬éä¸è¶³"
)
return current_user
return role_checker
ç¨½æ ¸ç´é (ISO 27001)
utils/audit.py
from sqlmodel import Session
from datetime import datetime
from typing import Optional
import json
from app.models.audit import AuditLog
def log_audit(
session: Session,
user_id: str,
action: str,
resource_type: str,
resource_id: str,
details: Optional[dict] = None,
ip_address: Optional[str] = None,
):
"""è¨éç¨½æ ¸æ¥èª"""
audit_log = AuditLog(
user_id=user_id,
action=action,
resource_type=resource_type,
resource_id=resource_id,
details=json.dumps(details) if details else None,
ip_address=ip_address,
timestamp=datetime.utcnow(),
)
session.add(audit_log)
session.commit()
é¯èª¤èç
å ¨åä¾å¤èç
from fastapi import Request
from fastapi.responses import JSONResponse
from sqlalchemy.exc import IntegrityError
@app.exception_handler(HTTPException)
async def http_exception_handler(request: Request, exc: HTTPException):
return JSONResponse(
status_code=exc.status_code,
content={
"success": False,
"error": {
"code": str(exc.status_code),
"message": exc.detail
}
}
)
@app.exception_handler(IntegrityError)
async def integrity_error_handler(request: Request, exc: IntegrityError):
return JSONResponse(
status_code=400,
content={
"success": False,
"error": {
"code": "INTEGRITY_ERROR",
"message": "è³æå®æ´æ§é¯èª¤ï¼å¯è½æ¯éè¤çè³æ"
}
}
)
@app.exception_handler(Exception)
async def general_exception_handler(request: Request, exc: Exception):
# è¨éé¯èª¤
print(f"Unhandled error: {exc}")
return JSONResponse(
status_code=500,
content={
"success": False,
"error": {
"code": "INTERNAL_ERROR",
"message": "伺æå¨å
§é¨é¯èª¤"
}
}
)
Render é¨ç½²
render.yaml
services:
- type: web
name: mes-api
runtime: python
buildCommand: pip install -r requirements.txt
startCommand: uvicorn app.main:app --host 0.0.0.0 --port $PORT
envVars:
- key: DATABASE_URL
fromDatabase:
name: mes-db
property: connectionString
- key: JWT_SECRET
generateValue: true
healthCheckPath: /health
autoDeploy: true
databases:
- name: mes-db
plan: free
databaseName: mes
user: mes_user
requirements.txt
fastapi>=0.109.0
uvicorn[standard]>=0.27.0
sqlmodel>=0.0.14
pydantic-settings>=2.1.0
python-jose[cryptography]>=3.3.0
psycopg2-binary>=2.9.9
ç¦æ¢äºé
- ç¦æ¢ Fallback Mock è³æ – è³æåº«é£ç·å¤±ææåå³ 503
- ç¦æ¢ Emoji – ç¨å¼ç¢¼ã註解é½ä¸ä½¿ç¨
- ç¦æ¢ç°¡é«å – é¯èª¤è¨æ¯ä½¿ç¨æ£é«ä¸æ
- ç¦æ¢ç¡¬ç·¨ç¢¼æ©æè³æ – 使ç¨ç°å¢è®æ¸
æè½æ¨æº
| ææ¨ | ç®æ¨ |
|---|---|
| API åææé | < 500ms |
| è³æåº«æ¥è©¢ | < 100ms |
| ååæé | < 30s |