2025年6月19日 星期四

PHP 工程師轉型之路:用 Python 和 FastAPI 打造你的第一個電商網站

PHP 工程師轉型之路:用 Python 和 FastAPI 打造現代電商網站 — 以 VueFastMart 為例

身為經驗豐富的 PHP 工程師,你可能已經在 Laravel、Symfony 或 CodeIgniter 的世界裡游刃有餘。但你是否曾思考,如何運用 Python 的簡潔優雅與 FastAPI 的極致性能,來打造一個兼具現代化與高效能的應用?本教學將以你的 VueFastMart 專案為核心,帶你從熟悉的 PHP 思維出發,一步步深入理解並實作一個基於 Python FastAPI 和 Vue.js 的現代電商產品管理系統。

我們將把你在 PHP 中習以為常的路由、ORM、認證、快取和部署等概念,完美對應到 Python 與 FastAPI 的生態中,助你快速掌握這項未來感十足的全端開發技能!

為什麼 PHP 工程師值得學習 Python 與 FastAPI?

你或許會問,為何要從熟悉的 PHP 生態轉向 Python 和 FastAPI?原因如下:

  • FastAPI 的卓越性能與開發體驗
    • 閃電般的速度:FastAPI 建立在 Starlette (用於 Web 部分) 和 Pydantic (用於資料驗證) 之上,原生支援異步操作(async/await),尤其在處理 I/O 密集型任務時,能展現出驚人的吞吐量,遠超傳統同步框架。
    • 自動化 API 文件:你只需編寫帶有型別提示的程式碼,FastAPI 便會自動生成 Swagger UI 和 ReDoc 互動式 API 文件,這在 PHP 中通常需要額外工具或手動維護。
    • 強大的型別提示與資料驗證:Pydantic 與 Python 型別提示的結合,提供了自動的請求資料驗證、錯誤處理和回應資料序列化,極大地減少了樣板程式碼,並提升了程式碼品質和開發效率。
  • Python 的廣泛應用與生態系
    • Python 不僅是 Web 開發利器,它在資料科學、機器學習、自動化、DevOps 等領域都扮演著核心角色。掌握 Python 將為你打開更廣闊的職業發展道路。
    • 與 PHP 豐富的 Composer 包類似,Python 擁有龐大的 PyPI 軟體包倉庫,幾乎任何功能都能找到現成的解決方案。
  • 程式碼的簡潔與可讀性:Python 以其清晰、簡潔的語法而聞名,有時能用更少的程式碼實現相同的功能,進一步提升了團隊協作和維護效率。

VueFastMart 專案概覽

VueFastMart 是一個基於 Vue.js 和 FastAPI 的前後端分離電商產品管理系統。此專案專為展示現代全端開發能力而設計,提供完善的產品管理、用戶認證和購物車功能,並著重於 可測試性安全性效能

專案核心功能詳解

VueFastMart 不僅僅是一個簡單的 CRUD 應用,它還包含了許多現代 Web 應用所需的關鍵特性:

一、前端功能 (Vue.js)

  • 產品列表與詳情
    • 清晰展示產品名稱、描述、價格、庫存等資訊。
    • 支援圖片懶加載 (Lazy Load),優化圖片載入速度,提升用戶體驗。
  • 購物車管理
    • 直觀地將產品加入/移除購物車。
    • 與後端 API 實時同步購物車狀態,確保資料一致性。
    • 利用 Pinia 進行高效的前端狀態管理。
  • 用戶認證流程
    • 提供完整的註冊和登入頁面,支援用戶身份驗證。
    • 透過 JWT (JSON Web Tokens) 進行認證,確保 API 請求的安全。
  • 響應式設計
    • 採用 Tailwind CSS 構建,確保界面在不同螢幕尺寸(手機、平板、桌面)上都能完美呈現。

二、後端 API (FastAPI)

後端 API 是專案的核心,採用 RESTful 風格設計,並透過明確的端點實現各種業務邏輯:

  • 認證相關 (/auth):
    • POST /auth/register: 用戶註冊功能。
      • 請求:電子郵件 (email) 和密碼 (password,至少 8 個字符,包含字母和數字)。
      • 回應:新創建用戶的資料。
    • POST /auth/token: 用戶登入並獲取 JWT 訪問令牌。
      • 請求:用戶名 (username,即電子郵件) 和密碼 (password)。
      • 回應:access_token (JWT) 和 token_type (bearer)。
    • GET /auth/users/me: 獲取當前登入用戶的資訊(需提供有效的 JWT)。
      • 回應:當前用戶的 id, email, is_admin 等。
  • 產品管理 (/products):
    • GET /products/: 分頁查詢產品列表。
      • 查詢參數:skip (跳過數量), limit (返回數量), q (按名稱模糊搜索)。
      • 優化:支援 Redis 快取,產品列表查詢快取 60 秒,提升性能。
    • GET /products/{id}: 獲取單一產品詳情。
    • POST /products/: 管理員專用,新增產品。
      • 請求:name, description, price, stock, image_url
    • PUT /products/{id}: 管理員專用,更新指定產品資訊。
      • 請求:可更新 name, description, price, stock, image_url
    • DELETE /products/{id}: 管理員專用,刪除指定產品。
  • 購物車管理 (/cart):
    • POST /cart/: 將產品添加到當前用戶的購物車。
      • 請求:product_id, quantity
      • 自動檢查產品庫存並更新。
    • GET /cart/: 獲取當前用戶的購物車內容。
      • 回應:購物車中每個商品的詳情及總價。
    • DELETE /cart/{id}: 從購物車中移除指定項目,並恢復對應庫存。
  • 健康檢查 (/health):
    • GET /health: 檢查後端 API 和資料庫連線狀態。
      • 回應:{"status": "ok", "database": "connected"} 或錯誤資訊。

三、性能與安全

  • Redis 快取:產品列表查詢使用 Redis 快取 60 秒,大幅提升 API 響應速度。
  • JWT 認證:所有敏感端點都透過 JWT 進行保護,確保只有合法用戶才能訪問。
  • Bcrypt 密碼加密:用戶密碼在資料庫中以 Bcrypt 雜湊形式儲存,極大提高了安全性。
  • CORS 和安全標頭:配置 Cross-Origin Resource Sharing (CORS) 以允許前端訪問,並添加 Content-Security-Policy, X-Content-Type-Options, X-Frame-Options 等 HTTP 安全標頭,有效防範常見 Web 漏洞。
  • 環境變數管理:所有敏感資訊(如數據庫連接字符串、JWT 密鑰)都通過環境變數管理,不硬編碼在程式碼中。

四、可測試性與部署

  • 後端測試 (Pytest):使用 Pytest 框架,對後端 API 進行全面的單元測試和集成測試,測試覆蓋率達到 80%。
  • 前端測試 (Vitest):使用 Vitest 框架,對前端 Vue 組件進行測試,確保核心功能穩定可靠。
  • 容器化部署 (Docker):提供完整的 docker-compose.yaml 配置,方便一鍵啟動所有服務 (FastAPI 後端, Vue 前端, PostgreSQL 資料庫, Redis 快取)。
  • 雲端部署指南:提供在 Vercel (前端) 和 Render (後端) 上的部署教學,讓你可以輕鬆將專案發布到線上。

技術棧一覽

  • 後端
    • FastAPI:基於 Starlette 和 Pydantic 的現代、快速 (高效能) Web 框架。
    • SQLAlchemy:強大的 Python SQL 工具包和 ORM (Object Relational Mapper),用於資料庫操作。
    • PostgreSQL:功能豐富、穩定可靠的開源關聯式資料庫系統(本地開發可選 SQLite)。
    • JWT (Python-Jose):用於基於 Token 的用戶認證。
    • Redis (redis-py):高性能的鍵值儲存資料庫,用於快取。
    • pytest:流行的 Python 測試框架,提供豐富的測試功能。
    • Gunicorn (生產環境):高效的 Python WSGI HTTP Server,用於運行 FastAPI 應用。
  • 前端
    • Vue.js 3 (Composition API):漸進式 JavaScript 框架,用於構建用戶界面,採用 Composition API 提升代碼組織性。
    • Pinia:輕量級、型別安全的 Vue 狀態管理庫,類似於 Vuex。
    • Vue Router:Vue.js 的官方路由管理器。
    • Axios:基於 Promise 的 HTTP 客戶端,用於前後端通信。
    • Tailwind CSS:實用至上的 CSS 框架,用於快速構建響應式 UI。
    • Vite:下一代前端構建工具,提供極速開發體驗。
    • Vitest:Vue 官方推薦的快速單元測試框架。
  • 其他
    • DockerDocker Compose:用於容器化開發和部署。
    • GitHub Actions:用於持續集成/持續部署 (CI/CD)。

第一步:自動創建專案骨架 (create_project.py)

在開始之前,強烈建議你使用提供的 create_project.py 腳本來自動生成整個 VueFastMart 專案的檔案和目錄結構。這將省去你手動創建大量文件和目錄的繁瑣步驟,並確保你的專案結構與教學內容完全一致。

  1. 下載 create_project.py 檔案:

    將 create_project.py 檔案下載到你的電腦上一個方便的位置(例如,你的桌面或專案工作區)。

  2. 打開終端機或命令提示字元 (CMD):

    導航到你下載 create_project.py 檔案的目錄。

    Bash
    cd /path/to/your/download/directory
    
  3. 執行腳本:

    運行以下命令:

    Bash
    python create_project.py
    

    如果你的系統同時安裝了 Python 2 和 Python 3,並且 python 命令預設指向 Python 2,請使用 python3

    Bash
    python3 create_project.py
    

    腳本會輸出創建檔案的進度。完成後,你將在當前目錄下看到一個名為 VueFastMart 的新資料夾。這個資料夾就是專案的完整骨架。

    提示:如果重複執行此腳本,它將會覆蓋已存在的同名文件,請注意備份你的修改。

第二步:環境設置與 PHP 思維轉換

在 PHP 專案中,你可能習慣直接透過 Composer 管理依賴,或使用 Laragon/XAMPP 等整合環境。在 Python/JavaScript 世界,我們通常這樣做:

  1. 進入新創建的 VueFastMart 專案目錄

    Bash
    cd VueFastMart
    
  2. 配置環境變數:

    複製範例環境變數文件並進行編輯。這些文件類似於 Laravel 專案的 .env。

    Bash
    cp backend/.env.example backend/.env
    cp frontend/.env.example frontend/.env
    

    然後,打開 backend/.envfrontend/.env 文件,根據你的需求設置環境變數。對於本地開發,你可以保留預設值以快速啟動 Docker Compose。

    • backend/.env (關鍵配置,用於 JWT 密鑰和資料庫連接)
      程式碼片段
      DATABASE_URL=postgresql://user:password@db:5432/vuefastmart_db # Docker 模式下,host 是 db
      SECRET_KEY=your-very-secure-secret-key-please-change-this-in-production # **務必修改為一個複雜的隨機字串**
      FRONTEND_URL=http://localhost:5173 # 或你的 Vercel 部署地址
      REDIS_HOST=redis # Docker 模式下,host 是 redis
      REDIS_PORT=6379
      
    • frontend/.env (關鍵配置,指向你的後端 API)
      程式碼片段
      VITE_API_URL=http://localhost:8000 # 或你的 Render 部署地址
      

第三步:本地安裝與執行

你可以選擇使用 Docker Compose 進行一鍵啟動,或手動配置和運行前後端。

選項 A: 使用 Docker Compose (推薦)

Docker Compose 提供了一個隔離且一致的開發環境,無需擔心本地環境衝突。

  1. 確保環境變數已配置:

    確認 backend/.env 和 frontend/.env 檔案存在且已根據「第二步」正確配置。

  2. 啟動所有服務:

    在 VueFastMart 專案的根目錄下,執行:

    Bash
    docker-compose up --build -d
    
    • --build: 首次運行時會構建 Docker 映像。
    • -d: 在後台運行服務。
  3. 檢查服務狀態

    Bash
    docker-compose ps
    

    你會看到 backendfrontenddbredis 服務都已啟動。

  4. 初始化資料庫:

    FastAPI 應用在啟動時會自動執行 Base.metadata.create_all(bind=engine) 來創建所有資料庫表格。所以一旦後端服務啟動,資料庫就會自動初始化。

  5. 訪問應用

    • 後端 API 文檔 (Swagger UI):http://localhost:8000/docs
    • 後端 API 健康檢查:http://localhost:8000/health
    • 前端應用:http://localhost:5173

選項 B: 手動本地執行 (不使用 Docker)

如果你更傾向於在本地環境手動配置,請確保你已安裝 Node.js (18+), Python (3.11+), PostgreSQL, Redis

設置後端
  1. 進入後端目錄

    Bash
    cd backend
    
  2. 創建 Python 虛擬環境 (venv):

    這類似於 PHP 專案的 vendor 資料夾,但它更嚴格地隔離了 Python 環境,確保每個專案的依賴是獨立的。

    Bash
    python3 -m venv venv
    source venv/bin/activate  # Windows 請使用 `venv\Scripts\activate`
    

    你會看到終端機提示符前面多了一個 (venv),表示你已進入這個專案專屬的 Python 環境。

  3. 安裝依賴:

    你的 requirements.txt 文件就是 Python 的 composer.json,它列出了專案所需的所有 Python 套件。

    Bash
    pip install -r requirements.txt
    
  4. 設置資料庫 (PostgreSQL 或 SQLite)

    • PostgreSQL (推薦):確保您本地安裝並啟動了 PostgreSQL 伺服器,並創建一個名為 vuefastmart_db 的資料庫。然後修改 backend/.env 中的 DATABASE_URL,將 db 改為 localhost
      程式碼片段
      DATABASE_URL=postgresql://user:password@localhost:5432/vuefastmart_db
      
    • SQLite (更簡單的本地開發選項):如果想快速上手,可以將 backend/.env 中的 DATABASE_URL 改為 sqlite:///./sql_app.db。FastAPI 會自動創建一個 sql_app.db 文件。
  5. 啟動後端

    Bash
    uvicorn app.main:app --reload
    

    後端 API 文檔將在 http://localhost:8000/docs 可用,健康檢查在 http://localhost:8000/health

設置前端
  1. 進入前端目錄
    Bash
    cd frontend
    
  2. 安裝依賴
    Bash
    npm install
    
  3. 配置環境變數
    Bash
    cp .env.example .env
    
    編輯 .env,設置 VITE_API_URL 指向你的後端,如果後端在本地運行,則為:
    程式碼片段
    VITE_API_URL=http://localhost:8000
    
  4. 啟動前端
    Bash
    npm run dev
    
    前端應用將在 http://localhost:5173 可用。

第四步:後端 API 開發 (FastAPI 對比 PHP 框架)

現在,讓我們深入後端程式碼,看看 FastAPI 如何實現你在 PHP 框架中常見的功能。

1. 資料庫連接與 ORM (SQLAlchemy vs. Eloquent/Doctrine)

在 PHP 中,你可能習慣使用 Laravel 的 Eloquent 或 Doctrine ORM 來操作資料庫。FastAPI 常用 SQLAlchemy,它是一個更底層但功能強大的 ORM。

PHP (Laravel Eloquent 概念):

通常透過 config/database.php 或 .env 配置資料庫連接,並定義繼承 Model 的 Eloquent 模型。

PHP
// app/Models/Product.php (Laravel)
class Product extends Model {
    protected $table = 'products';
    // ... 其他屬性
}
// 使用:Product::all(); Product::find(1);

Python (FastAPI + SQLAlchemy 實現) - backend/app/database.py

Python
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from dotenv import load_dotenv
import os

load_dotenv()
# 從 .env 讀取資料庫 URL,類似於 Laravel 從 .env 讀取 DB_HOST 等
DATABASE_URL = os.getenv("DATABASE_URL", "postgresql://user:password@localhost:5432/vuefastmart_db")

# 建立引擎,類似於資料庫連線實例
engine = create_engine(DATABASE_URL)
# 建立 Session,類似於每個請求的資料庫事務 (Database Transaction)
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
# ORM 基礎類別,所有模型都會繼承它 (類似於 Eloquent 的 Model 類)
Base = declarative_base()

# **依賴注入函數**:這是一個 FastAPI 的核心概念
# 每次請求時,FastAPI 會調用 get_db 並提供一個資料庫會話,請求結束後會自動關閉
def get_db():
    db = SessionLocal() # 獲取一個新的會話
    try:
        yield db # 'yield' 關鍵字讓這個函數成為一個生成器,FastAPI 會自動管理會話的生命週期
    finally:
        db.close() # 請求結束後關閉會話

模型定義 (backend/app/models/product.py 等)

Python
# backend/app/models/product.py
from sqlalchemy import Column, Integer, String, Float, Text, Index
from app.database import Base # 引入 ORM 基礎類別

class Product(Base): # 繼承 Base,類似於 Eloquent Model
    __tablename__ = "products" # 對應的資料庫表名

    id = Column(Integer, primary_key=True, index=True)
    name = Column(String, index=True)
    description = Column(Text, nullable=True)
    price = Column(Float, nullable=False)
    stock = Column(Integer, nullable=False)

    __table_args__ = (Index('idx_name', 'name'),) # 定義索引
  • 思維轉換:在 PHP ORM 中,你直接調用 Product::all()Product::where('id', 1)->first()。在 SQLAlchemy 中,你需要先透過 FastAPI 的依賴注入獲取一個 db 會話物件,然後使用 db.query(Product).all()db.query(Product).filter(Product.id == 1).first() 來查詢。雖然語法不同,但核心思想都是透過物件操作資料庫,且 SQLAlchemy 提供更細粒度的控制。

2. 資料驗證與序列化 (Pydantic vs. Laravel Request Validation/Resource)

在 PHP 框架中,你常用表單請求 (Form Request) 進行輸入驗證,並用 API Resource 進行輸出資料的格式化。FastAPI 則透過 Pydantic 提供原生支援。

PHP (Laravel Request / Resource 概念)

PHP
// app/Http/Requests/StoreProductRequest.php
public function rules() {
    return ['name' => 'required|string|max:255', 'price' => 'required|numeric'];
}

// app/Http/Resources/ProductResource.php
public function toArray($request) {
    return ['id' => $this->id, 'name' => $this->name];
}

Python (FastAPI + Pydantic 實現) - backend/app/schemas/product.py

Python
from pydantic import BaseModel, Field # 引入 BaseModel 定義資料模型,Field 用於更詳細的驗證規則

class ProductBase(BaseModel): # 定義基礎屬性,通常用於輸入數據
    name: str = Field(min_length=3, max_length=100) # 類似 'required|string|min:3|max:100'
    description: str | None = None # Python 3.10+ 的 Union Type,表示可以是 str 或 None (nullable)
    price: float = Field(gt=0) # 'required|numeric|gt:0' (大於 0)
    stock: int = Field(ge=0) # 'required|integer|ge:0' (大於等於 0)
    image_url: str | None = None # 可選的圖片 URL

class ProductCreate(ProductBase): # 創建產品時的輸入驗證模型 (通常繼承 ProductBase)
    pass # 繼承 ProductBase 的所有屬性,並可額外添加

class Product(ProductBase): # API 返回給前端的產品格式 (包含 ID)
    id: int # 輸出時必須包含 ID
    class Config:
        from_attributes = True # Pydantic v2 用此設定,允許從 SQLAlchemy ORM 物件自動轉換為此 Pydantic 模型
  • 思維轉換:FastAPI 透過 Pydantic 的型別提示和 Field 驗證器自動執行驗證和序列化。你只需要定義好 BaseModel,FastAPI 就會幫你處理資料的輸入驗證(如果輸入不符會自動返回 422 Unprocessable Entity 錯誤)和輸出格式化。這比手動編寫驗證規則和 Resource 類別更為簡潔和自動化,並且在開發階段就能獲得更好的 IDE 提示。

3. 路由與控制器 (APIRouter vs. Laravel Routes/Controllers)

在 PHP 中,你透過 web.phpapi.php 定義路由,並將請求導向控制器方法。在 FastAPI 中,我們使用 APIRouter

PHP (Laravel Route/Controller 概念)

PHP
// routes/api.php (Laravel)
Route::get('/products', [ProductController::class, 'index']);

// app/Http/Controllers/ProductController.php (Laravel)
class ProductController extends Controller {
    public function index(Request $request) {
        return Product::paginate($request->limit);
    }
}

Python (FastAPI 實現) - backend/app/api/products.py

Python
from fastapi import APIRouter, Depends, HTTPException, Query # 引入 Query 用於參數驗證
from sqlalchemy.orm import Session
from sqlalchemy import select
from app.models.product import Product as ProductModel
from app.schemas.product import Product, ProductCreate # 引入 Pydantic 格式
from app.database import get_db # 引入資料庫依賴注入
from app.api.auth import get_current_user # 引入用戶認證依賴
from app.models.user import User
from app.cache import cache # 引入快取裝飾器

router = APIRouter() # 類似於 Laravel 的 Route::prefix() 或 Route::group()

@router.get("/", response_model=list[Product]) # 定義 GET 請求,回傳 Product 列表
@cache(timeout=60) # 這是一個自定義的快取裝飾器,會自動快取響應!
async def get_products( # 使用 async def 實現異步函數
    skip: int = Query(0, ge=0), # 查詢參數,類似於 $_GET['skip'],並加上驗證
    limit: int = Query(10, gt=0, le=100), # 查詢參數,類似於 $_GET['limit'],並加上驗證
    q: str | None = Query(None, min_length=1, max_length=50), # 搜索查詢參數
    db: Session = Depends(get_db) # **依賴注入**:FastAPI 會自動提供一個資料庫會話
):
    query = select(ProductModel)
    if q:
        query = query.filter(ProductModel.name.ilike(f"%{q}%")) # 模糊搜索
    products = db.execute(query.offset(skip).limit(limit)).scalars().all()
    return products

@router.post("/", response_model=Product) # 定義 POST 請求,回傳創建後的 Product
async def create_product( # 使用 async def 實現異步函數
    product: ProductCreate, # FastAPI 會自動驗證請求體是否符合 ProductCreate 格式
    db: Session = Depends(get_db),
    current_user: User = Depends(get_current_user) # **依賴注入**:獲取當前登入用戶
):
    if not current_user.is_admin: # 檢查用戶權限,類似於 PHP 的 Middleware 或 Gate
        raise HTTPException(status_code=403, detail="僅管理員可新增產品")
    db_product = ProductModel(**product.model_dump()) # Pydantic v2 使用 .model_dump() 轉換為字典
    db.add(db_product)
    db.commit()
    db.refresh(db_product)
    return db_product
  • 思維轉換:FastAPI 的 @router.get("/") 就像 Laravel 的 Route::get('/products', ...)Depends() 是 FastAPI 強大的依賴注入機制,它會自動將參數所需的值(例如 dbcurrent_user)傳入函數,類似於你在 Laravel 控制器方法中型別提示 Request $requestProduct $productasync def 讓你的 API 可以非阻塞地處理 I/O 操作(例如資料庫查詢或外部 API 請求),極大提升了應用程式的並發能力。自定義的 @cache 裝飾器則是一個高級特性,在 PHP 中通常需要手動實現或使用框架提供的快取 facade。

4. 用戶認證與權限控制 (JWT & bcrypt vs. Laravel Auth/Passport)

在 PHP 中,你可能使用 Laravel 的內建認證系統或 Passport/Sanctum 處理 API 認證。FastAPI 則常用 JWT (JSON Web Tokens)bcrypt

PHP (Laravel Auth 概念):

通常透過 config/auth.php 配置認證守衛,並使用 Auth::attempt() 進行登入驗證,Hash::make() 雜湊密碼。

PHP
// app/Http/Controllers/AuthController.php (Laravel)
class AuthController extends Controller {
    public function login(Request $request) {
        if (Auth::attempt(['email' => $request->email, 'password' => $request->password])) {
            // ... 生成 token
        }
    }
}

Python (FastAPI 實現) - backend/app/api/auth.py

Python
from fastapi import APIRouter, Depends, HTTPException, status
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm # 用於自動處理登入表單
from sqlalchemy.orm import Session
from passlib.context import CryptContext # 雜湊密碼,類似於 PHP 的 password_hash() / bcrypt
from jose import JWTError, jwt # JWT 庫,用於編碼和解碼 Token
from datetime import datetime, timedelta
from dotenv import load_dotenv
from app.database import get_db
from app.models.user import User # 引入 ORM 模型
from app.schemas.user import UserCreate, User as UserSchema # 引入 Pydantic 驗證模型
import os

load_dotenv()

router = APIRouter()
SECRET_KEY = os.getenv("SECRET_KEY", "your-very-secure-secret-key") # 從環境變數讀取密鑰
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") # 定義 bcrypt 雜湊方式
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="auth/token") # 定義 OAuth2 認證方案,告訴 Swagger UI 如何獲取 Token

def verify_password(plain_password, hashed_password):
    return pwd_context.verify(plain_password, hashed_password) # 驗證密碼,類似於 PHP 的 password_verify()

def get_password_hash(password):
    return pwd_context.hash(password) # 雜湊密碼

def create_access_token(data: dict): # 創建 JWT token
    to_encode = data.copy()
    expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
    to_encode.update({"exp": expire}) # 將過期時間加入 payload
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)):
    # 這是 FastAPI 的認證依賴,它會從請求頭部解析 JWT token (Authorization: Bearer <token>)
    credentials_exception = HTTPException(
        status_code=status.HTTP_401_UNAUTHORIZED,
        detail="無效的認證憑證",
        headers={"WWW-Authenticate": "Bearer"},
    )
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) # 解碼 JWT
        email: str = payload.get("sub") # 獲取 Token 中的用戶資訊
        if email is None:
            raise credentials_exception
    except JWTError: # 如果 Token 無效或過期,則拋出 JWTError
        raise credentials_exception
    user = db.query(User).filter(User.email == email).first() # 根據 Token 中的資訊查詢用戶
    if user is None:
        raise credentials_exception
    return user # 返回用戶物件,供後續 API 使用

@router.post("/register", response_model=UserSchema)
async def register_user(user: UserCreate, db: Session = Depends(get_db)):
    db_user = db.query(User).filter(User.email == user.email).first()
    if db_user:
        raise HTTPException(status_code=400, detail="電子郵件已被註冊")
    if len(user.password) < 8 or not any(char.isalpha() for char in user.password) or not any(char.isdigit() for char in user.password):
        raise HTTPException(status_code=400, detail="密碼長度需至少8個字符,且包含字母和數字")
    
    hashed_password = get_password_hash(user.password) # 雜湊密碼
    db_user = User(email=user.email, hashed_password=hashed_password, is_admin=False) # 預設非管理員
    db.add(db_user)
    db.commit()
    db.refresh(db_user)
    return db_user

@router.post("/token") # 登入 API
async def login(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
    user = db.query(User).filter(User.email == form_data.username).first()
    if not user or not verify_password(form_data.password, user.hashed_password):
        raise HTTPException(
            status_code=status.HTTP_401_UNAUTHORIZED,
            detail="電子郵件或密碼不正確",
            headers={"WWW-Authenticate": "Bearer"},
        )
    access_token = create_access_token(data={"sub": user.email, "is_admin": user.is_admin}) # 創建 token,可包含用戶角色
    return {"access_token": access_token, "token_type": "bearer"}

@router.get("/users/me", response_model=UserSchema)
async def read_users_me(current_user: User = Depends(get_current_user)):
    return current_user
  • 思維轉換OAuth2PasswordBearerDepends(oauth2_scheme) 簡化了從請求頭部獲取 Token 的過程。get_current_user 函數作為一個依賴,可以被其他路由使用,類似於 PHP 的 Middleware,確保只有已認證的用戶才能訪問。角色權限檢查 (if not current_user.is_admin:) 則是在 API 函數內部進行,非常清晰且易於維護。

5. 應用程式入口與 CORS (backend/app/main.py)

在 PHP 框架中,通常有一個 public/index.php 作為入口,而 CORS 配置則在中間件或 Web 伺服器中設定。

Python (FastAPI 實現) - backend/app/main.py

Python
from fastapi import FastAPI, Depends, HTTPException, status
from fastapi.middleware.cors import CORSMiddleware # CORS 中間件
from dotenv import load_dotenv
from sqlalchemy.orm import Session
from sqlalchemy import text # 用於資料庫健康檢查
import os

# 引入路由模組
from app.api import auth, products, cart
from app.database import engine, Base, get_db # 引入資料庫引擎和 Base

load_dotenv()

app = FastAPI(
    title="VueFastMart API",
    description="一個基於 FastAPI 和 Vue.js 的現代電商產品管理系統。",
    version="1.0.0",
) # 創建 FastAPI 應用實例,可添加元資料

# 配置 CORS,類似於 Laravel 的 cors.php 設定
app.add_middleware(
    CORSMiddleware,
    allow_origins=[os.getenv("FRONTEND_URL", "http://localhost:5173")], # 允許的前端來源,從環境變數讀取
    allow_credentials=True, # 允許傳遞 Cookie (對於 JWT 通常不是必須的,但對其他認證方式有用)
    allow_methods=["*"], # 允許所有 HTTP 方法 (GET, POST, PUT, DELETE 等)
    allow_headers=["*"], # 允許所有頭部 (Authorization, Content-Type 等)
)

# 添加安全頭部 (Middleware),增強應用安全性
@app.middleware("http")
async def add_security_headers(request, call_next):
    response = await call_next(request)
    response.headers["Content-Security-Policy"] = "default-src 'self'" # CSP 頭部,限制資源來源
    response.headers["X-Content-Type-Options"] = "nosniff" # 防止瀏覽器 MIME 嗅探
    response.headers["X-Frame-Options"] = "DENY" # 防止點擊劫持
    response.headers["Strict-Transport-Security"] = "max-age=31536000; includeSubDomains" # HSTS,強制 HTTPS
    return response

# 在應用啟動時創建資料庫表格 (類似於 PHP 的 Artisan migrate)
# FastAPI 應用在啟動時會運行這段程式碼
@app.on_event("startup")
def on_startup():
    print("Creating database tables...")
    Base.metadata.create_all(bind=engine) # 根據 ORM 模型創建所有資料庫表格
    print("Database tables created.")

# 註冊 API 路由模組
app.include_router(auth.router, prefix="/auth", tags=["認證"])
app.include_router(products.router, prefix="/products", tags=["產品"])
app.include_router(cart.router, prefix="/cart", tags=["購物車"])

@app.get("/")
def read_root():
    return {"message": "歡迎使用 VueFastMart API - 前往 /docs 查看 API 文件"}

@app.get("/health")
def health_check(db: Session = Depends(get_db)):
    """
    檢查 API 和資料庫連線狀態。
    """
    try:
        db.execute(text("SELECT 1")) # 簡單查詢,檢查資料庫連接是否活躍
        return {"status": "ok", "database": "connected"}
    except Exception as e:
        raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=f"資料庫連接失敗: {e}")
  • 思維轉換FastAPI 實例就是你的應用程式核心。app.add_middleware 實現了類似 PHP 中間件的功能,用於處理跨域請求和添加安全頭部。@app.on_event("startup") 裝飾器確保在應用啟動時執行資料庫表格的創建,這正是 php artisan migrate 的自動化版本。新增的 /health 端點是一個很好的實踐,你可以用它來監控後端服務的狀態,在容器化和微服務架構中尤其重要。

6. 運行後端服務

在 PHP 中,你可能習慣使用 php artisan serve 或 Apache/Nginx + PHP-FPM。FastAPI 通常使用 uvicorn (開發) 或 gunicorn (生產環境)。

Bash
# 在 backend 目錄下
# 開發模式,自動重載
uvicorn app.main:app --reload --host 0.0.0.0 --port 8000

# 生產模式 (搭配 gunicorn,通常在 Docker 環境中使用)
# gunicorn -w 4 -k uvicorn.workers.UvicornWorker app.main:app --bind 0.0.0.0:8000
  • 思維轉換uvicorn 類似於 Laravel 的開發伺服器,方便即時修改。gunicorn 則類似於 php-fpm,用於生產環境處理高併發。--host 0.0.0.0 允許從外部訪問,這在 Docker 環境中尤其重要。

第五步:測試你的程式碼 (Pytest vs. PHPUnit)

在 PHP 中,你使用 PHPUnit 進行單元測試和集成測試。在 Python 中,pytest 是最受歡迎的測試框架。

PHP (PHPUnit 概念)

PHP
// tests/Unit/ProductTest.php (PHPUnit)
class ProductTest extends TestCase {
    public function testProductCreation() { /* ... */ }
}

Python (FastAPI + pytest 實現) - backend/tests/test_products.py1

Python
import pytest
import json
from fastapi.testclient import TestClient # FastAPI 提供的測試客戶端
from sqlalchemy import create_engine
from sqlalchemy.orm import sessionmaker
from app.main import app
from app.database import Base, get_db
from app.models.product import Product
from app.models.user import User # 引入用戶模型
from jose import jwt
from datetime import datetime, timedelta
from passlib.context import CryptContext
import os
import redis.asyncio as redis # 測試 Redis 快取時需要
from unittest.mock import AsyncMock, patch # 用於 mock 異步操作

# 使用記憶體中的 SQLite 資料庫進行測試,避免影響真實資料庫
DATABASE_URL = "sqlite:///:memory:"
engine = create_engine(DATABASE_URL)
TestingSessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)

# 覆寫 get_db 依賴,讓測試使用獨立的資料庫會話
def override_get_db():
    db = TestingSessionLocal()
    try:
        yield db
    finally:
        db.close()

# 將應用程式的 get_db 依賴替換為測試專用的 override_get_db
app.dependency_overrides[get_db] = override_get_db
client = TestClient(app) # 創建 FastAPI 測試客戶端,類似於 Laravel 的 $this->json() 或 $this->call()

pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
SECRET_KEY = os.getenv("SECRET_KEY", "your-test-secret-key") # 測試環境的密鑰

@pytest.fixture(scope="module") # 模組級別的 fixture,在測試文件開始前執行一次
def setup_database(): # 測試前創建資料庫表格,測試後清理
    Base.metadata.create_all(bind=engine)
    yield # 執行測試
    Base.metadata.drop_all(bind=engine) # 測試結束後刪除所有表格

@pytest.fixture # 定義一個 fixture,用於測試中自動提供一個管理員用戶
def admin_user():
    db = TestingSessionLocal()
    hashed_password = pwd_context.hash("securepassword")
    user = User(email="admin@example.com", hashed_password=hashed_password, is_admin=True)
    db.add(user)
    db.commit()
    db.refresh(user)
    token = jwt.encode(
        {"sub": user.email, "is_admin": user.is_admin}, SECRET_KEY, algorithm="HS256"
    ) # 生成管理員 Token
    db.close()
    return {"user": user, "token": token}

def test_create_product(setup_database, admin_user): # 測試新增產品功能
    response = client.post(
        "/products/",
        json={"name": "測試產品", "description": "這是一個測試產品", "price": 100.0, "stock": 10},
        headers={"Authorization": f"Bearer {admin_user['token']}"} # 傳遞認證 token
    )
    assert response.status_code == 200 # 斷言 HTTP 狀態碼
    data = response.json()
    assert data["name"] == "測試產品" # 斷言返回數據的正確性
    assert data["price"] == 100.0
    assert "id" in data # 確認返回數據中包含 ID

@patch('app.cache.redis_client.get', new_callable=AsyncMock) # Mock Redis get 方法
@patch('app.cache.redis_client.setex', new_callable=AsyncMock) # Mock Redis setex 方法
def test_get_products_cached(mock_setex, mock_get, setup_database, admin_user):
    # 模擬快取命中
    mock_get.return_value = json.dumps([{"id": 1, "name": "Cached Product", "description": None, "price": 50.0, "stock": 5}]).encode('utf-8')
    
    response = client.get(
        "/products/",
        headers={"Authorization": f"Bearer {admin_user['token']}"}
    )
    assert response.status_code == 200
    assert response.json()[0]["name"] == "Cached Product"
    mock_get.assert_called_once() # 確認有嘗試從快取中讀取
    mock_setex.assert_not_called() # 確認沒有寫入快取,因為已命中

    # 模擬快取未命中
    mock_get.return_value = None
    client.post(
        "/products/",
        json={"name": "新產品", "description": "描述", "price": 200.0, "stock": 20},
        headers={"Authorization": f"Bearer {admin_user['token']}"}
    )
    response = client.get(
        "/products/",
        headers={"Authorization": f"Bearer {admin_user['token']}"}
    )
    assert response.status_code == 200
    assert response.json()[0]["name"] == "新產品"
    mock_setex.assert_called_once() # 確認有寫入快取
  • 思維轉換TestClient(app) 讓你無需啟動真實伺服器就能測試 FastAPI 路由,類似於 Laravel 的 actingAs()json() 方法。@pytest.fixture 是一個強大的概念,它可以在測試函數運行前提供預設的設置或數據(如資料庫連接、測試用戶),這比 PHPUnit 中的 setUp() 方法更加靈活和模組化。@patch 裝飾器可以模擬(Mock)外部依賴(如 Redis 客戶端),讓你的測試更專注於單元邏輯,這在處理異步操作和外部服務時尤其有用。

運行測試指令

  • 後端測試 (在 backend 目錄下):
    Bash
    pytest tests/ --cov=app --cov-report=html
    
    --cov=app 用於生成測試覆蓋率報告,--cov-report=html 會在 htmlcov/ 目錄下生成 HTML 格式的報告,你可以在瀏覽器中打開查看。
  • 前端測試 (在 frontend 目錄下):
    Bash
    npm run test
    

第六步:部署指引

你的專案已經提供了 Vercel (前端) 和 Render (後端) 的部署範例,這對於 PHP 工程師來說,將會是一個很好的 CI/CD 和雲端部署的學習機會。

部署前端到 Vercel

  1. 將前端程式碼推送到 GitHub: 確保你的 frontend 目錄下的所有程式碼都已推送到一個 GitHub 倉庫。
  2. 在 Vercel 創建新專案: 登入 Vercel,選擇「Add New Project」,然後連接到你的前端 GitHub 倉庫。
  3. 配置構建設定 (通常 Vercel 會自動檢測)
    • 框架預設:Vercel 會自動檢測你的專案是 Vue 應用。
    • 構建指令npm run build
    • 輸出目錄dist
  4. 設置環境變數: 在 Vercel 專案設定中,進入「Environment Variables」區塊,添加一個關鍵環境變數:
    • VITE_API_URL: 填寫你的後端 API 公開可訪問的地址(例如,如果你已部署到 Render,可以是 https://your-render-app-name.onrender.com)。
  5. 部署: 點擊「Deploy」按鈕。Vercel 會自動拉取程式碼,執行構建指令,並將靜態檔案部署到全球 CDN。部署完成後,你會獲得一個前端的 URL。請將此 URL 更新到後端的 backend/.env 中的 FRONTEND_URL

部署後端到 Render

  1. 將後端程式碼推送到 GitHub: 確保你的 backend 目錄下的所有程式碼都已推送到一個 GitHub 倉庫。
  2. 在 Render.com 創建一個新的 Web Service: 登入 Render,選擇「New Web Service」,然後連接到你的後端 GitHub 倉庫。
  3. 配置服務設定
    • Name:給你的服務一個名稱 (例如 vuefastmart-api)。
    • Root Directory:選擇 backend/ (因為你的 FastAPI 程式碼在 backend 目錄下)。
    • Runtime:Python
    • Build Command (構建指令):
      Bash
      pip install -r requirements.txt
      
    • Start Command (啟動指令):
      Bash
      gunicorn -w 4 -k uvicorn.workers.UvicornWorker app.main:app --bind 0.0.0.0:$PORT
      
      這裡使用了 gunicorn,它是一個生產級的 WSGI 伺服器,可以處理多個工作進程,提高應用程式的併發處理能力。-w 4 表示啟動 4 個工作進程。$PORT 是 Render 自動提供的環境變數。
    • Scalability:可以設定實例類型和數量(建議從免費或最低級別開始)。
  4. 設置環境變數: 在 Render 服務設定中,進入「Environment Variables」區塊,添加以下關鍵環境變數:
    • DATABASE_URL: 填寫 Render PostgreSQL 資料庫的連接 URL (Render 會為你創建一個新的 PostgreSQL 資料庫並提供連接 URL)。
    • SECRET_KEY: 一個安全的密鑰字串,用於 JWT 加密(請使用與 Vercel 相同的複雜密鑰)。
    • FRONTEND_URL: 填寫你部署到 Vercel 的前端應用 URL (例如 https://your-vercel-app.vercel.app)。
    • REDIS_HOST: 如果使用 Render Redis,填寫其內部主機名 (Render 會為你創建並提供)。
    • REDIS_PORT: 填寫 Render Redis 的埠號。
  5. 部署: 點擊「Create Web Service」。Render 會自動拉取程式碼,執行構建和啟動指令。部署完成後,你會獲得一個後端的 API URL。

問題排查

在開發或部署過程中遇到問題是正常的。以下是一些常見問題及排查方向:

  • 後端無法啟動
    • 檢查 .env 檔案:確保 backend/.env 中的 SECRET_KEYDATABASE_URL 配置正確。
    • 資料庫連線:確認 PostgreSQL 或 SQLite 服務正在運行,且連接字符串無誤。
    • 依賴安裝:確認所有 Python 依賴都已透過 pip install -r requirements.txt 正確安裝。
    • 查看日誌:檢查 uvicorngunicorn 的終端機輸出,錯誤訊息通常會提供線索。
  • 前端無法訪問後端 API (CORS 錯誤)
    • 檢查 FRONTEND_URL:確保 backend/.env 中的 FRONTEND_URL 設置正確,包含了前端應用的完整 URL (包括協定和埠號)。如果有多個前端來源,可以在 FastAPI 的 CORSMiddleware 中添加多個 allow_origins
    • 檢查後端是否運行:確認後端 API 服務已成功啟動並正在監聽正確的埠。
  • 資料庫錯誤 (例如表不存在)
    • 運行 Base.metadata.create_all(bind=engine):確保此行程式碼在 backend/app/main.py 中被執行,或者你已手動執行資料庫遷移。在 Docker Compose 中,後端服務啟動時會自動執行。
  • 查看日誌
    • 後端:檢查 uvicorn 的終端機輸出。如果使用 Docker Compose,可以使用 docker-compose logs backend
    • 前端:使用瀏覽器開發者工具 (F12),查看「Console」和「Network」標籤,通常會有清晰的錯誤提示。

總結與展望

恭喜你!作為一名 PHP 工程師,你已經透過 VueFastMart 這個專案,初步掌握了 Python FastAPI 和 Vue.js 的核心概念和實踐。你應該會發現,儘管語法和一些工具不同,但背後的 Web 開發邏輯、MVC (或更準確地說,MVT/MVVM) 架構模式以及解決問題的思維是相通的。這是一次成功的技術棧擴展!

你的 GitHub 專案連結:https://github.com/BpsEason/VueFastMart.git

繼續探索以下內容,你的 Python 全端開發技能將會突飛猛進:

  • 異步編程 (Async/Await) 進階:深入理解 async/await 的工作原理,並將其應用於更多 I/O 密集型任務,如外部 API 調用。
  • SQLAlchemy 進階:學習更多 ORM 關係(多對多、多對一)、聯結查詢、事務管理等高級功能。
  • FastAPI 路由群組與標籤:更有效地組織和管理大型應用程式的 API。
  • FastAPI 依賴注入的更深層次應用:創建更複雜的依賴,例如分頁依賴、權限驗證依賴等。
  • 單元測試與集成測試覆蓋率提升:學習如何編寫更健壯的測試用例。
  • 部署策略優化:研究 Nginx/Caddy 作為反向代理、Docker Swarm 或 Kubernetes 進行容器編排等生產環境部署方案。
  • 性能監控與日誌管理:整合 Prometheus/Grafana 或 ELK Stack 等工具進行應用程式監控和日誌收集。

希望這篇文章能成為你從 PHP 邁向現代全端開發的堅實橋樑,祝你在新的技術旅程中取得更多成就!


沒有留言:

張貼留言

熱門文章