Go: مشروع شامل: واجهة برمجة تطبيقات التجارة الإلكترونية…

آخر تحديث: 2026-08-26

هذا كل ما يتعلق بالنظرية. والآن لنبدأ — في بناء واجهة برمجة تطبيقات (API) حقيقية للتجارة الإلكترونية من الصفر باستخدام لغة Go.

قرر بوب إنشاء منصة التجارة الإلكترونية الخاصة به. ثلاثة أسابيع، ثلاث قصص، ونظام خلفي كامل مكتوب بلغة Go.

1. ستتعلم



2. القصة: اليوم الأول لبوب كرائد أعمال

(1) المشكلة: «سأقوم بتطوير نموذج قابل للتطبيق (MVP) أولاً وأطلقه في غضون أسبوعين.»

استقال بوب من وظيفته لبدء مشروعه الخاص — منصة للتجارة الإلكترونية:

"في اليوم الأول، قمت بإنشاء برنامج متكامل — تضمن كل المنطق في main.go. وبحلول اليوم السابع، كان حجم الكود قد نما إلى 3,000 سطر، ولم أعد أستطيع التمييز بين الأجزاء التي تتعلق بمنطق التسلسل والأجزاء التي تتعلق بمنطق المستخدم. أردت إضافة ميزة «القسيمة»، لكن بعد تعديل ستة ملفات، لم أكن قد انتهيت بعد. قال شريكي في العمل: «أطلق المشروع في غضون أسبوعين»، لكنني أعتقد أن حتى شهرين قد يكونان فترة طويلة جدًا."

ولكن بعد أن هدأ وتأمل في بنية «Go» المكونة من أربع طبقات، قرر إعادة هيكلة الكود:

GO
// Refactoring plan:
// cmd/server/        → entry point
// internal/handler/  → HTTP handlers
// internal/service/  → business logic
// internal/repository/ → data access
// pkg/model/         → domain models

(2) أهداف الدرس: العمليات الأساسية لـ CRUD الخاصة بالمستخدمين والمنتجات والطلبات

هدف الأسبوع الأول: إنشاء إطار عمل كامل وتنفيذ عمليات CRUD للموارد الأساسية الثلاثة.

TEXT 📖 للعرض فقط
POST /api/v1/register         → User registration
POST /api/v1/login            → User login
GET  /api/v1/products         → Product list
GET  /api/v1/products/{id}    → Product details
POST /api/v1/orders           → Create order
GET  /api/v1/orders/{id}      → Order details
GET  /api/v1/orders           → My orders list


3. هيكل المشروع

TEXT 📖 للعرض فقط
ecommerce/
├── cmd/
│   └── server/
│       └── main.go              # Entry + dependency injection
├── internal/
│   ├── handler/                  # HTTP layer
│   │   ├── user.go
│   │   ├── product.go
│   │   ├── order.go
│   │   └── response.go          # Unified response utilities
│   ├── service/                  # Business layer
│   │   ├── user.go
│   │   ├── product.go
│   │   ├── order.go
│   │   └── errors.go            # Business error definitions
│   └── repository/              # Data layer
│       ├── user.go
│       ├── product.go
│       └── order.go
├── pkg/
│   └── model/                   # Domain models
│       ├── user.go
│       ├── product.go
│       └── order.go
├── go.mod
└── go.sum


4. التنفيذ الكامل

▶ مثال: طبقة النموذج

GO
// pkg/model/user.go
package model

type User struct {
    ID        int     `json:"id"`
    Email     string  `json:"email"`
    Password  string  `json:"-"`       // Not returned to client
    Name      string  `json:"name"`
}

type RegisterRequest struct {
    Email    string `json:"email"`
    Password string `json:"password"`
    Name     string `json:"name"`
}

type LoginRequest struct {
    Email    string `json:"email"`
    Password string `json:"password"`
}
▶ جرّب الكود
GO
// pkg/model/product.go
package model

type Product struct {
    ID          int     `json:"id"`
    Name        string  `json:"name"`
    Description string  `json:"description"`
    Price       float64 `json:"price"`
    Stock       int     `json:"stock"`
}
GO
// pkg/model/order.go
package model

type Order struct {
    ID         int       `json:"id"`
    UserID     int       `json:"user_id"`
    ProductID  int       `json:"product_id"`
    Quantity   int       `json:"quantity"`
    TotalPrice float64   `json:"total_price"`
    Status     string    `json:"status"`
}

type CreateOrderRequest struct {
    ProductID int `json:"product_id"`
    Quantity  int `json:"quantity"`
}

▶ مثال: طبقة المستودع

GO 📖 للعرض فقط
// internal/repository/user.go
package repository

import (
    "database/sql"
    "ecommerce/pkg/model"
)

type UserRepository interface {
    FindByID(id int) (*model.User, error)
    FindByEmail(email string) (*model.User, error)
    Create(user *model.User) error
}

type userRepository struct {
    db *sql.DB
}

func NewUserRepository(db *sql.DB) UserRepository {
    return &userRepository{db: db}
}

func (r *userRepository) FindByID(id int) (*model.User, error) {
    u := &model.User{}
    err := r.db.QueryRow("SELECT id, email, password, name FROM users WHERE id = ?", id).
        Scan(&u.ID, &u.Email, &u.Password, &u.Name)
    if err == sql.ErrNoRows {
        return nil, nil
    }
    return u, err
}

func (r *userRepository) FindByEmail(email string) (*model.User, error) {
    u := &model.User{}
    err := r.db.QueryRow("SELECT id, email, password, name FROM users WHERE email = ?", email).
        Scan(&u.ID, &u.Email, &u.Password, &u.Name)
    if err == sql.ErrNoRows {
        return nil, nil
    }
    return u, err
}

func (r *userRepository) Create(user *model.User) error {
    result, err := r.db.Exec(
        "INSERT INTO users (email, password, name) VALUES (?, ?, ?)",
        user.Email, user.Password, user.Name,
    )
    if err != nil {
        return err
    }
    id, _ := result.LastInsertId()
    user.ID = int(id)
    return nil
}
46 سطر من الكود المنطقي (تجاوز الحد 40, للعرض فقط)
GO
// internal/repository/product.go
package repository

import (
    "database/sql"
    "ecommerce/pkg/model"
)

type ProductRepository interface {
    FindByID(id int) (*model.Product, error)
    List() ([]*model.Product, error)
    Create(product *model.Product) error
    UpdateStock(id int, stock int) error
}

type productRepository struct {
    db *sql.DB
}

func NewProductRepository(db *sql.DB) ProductRepository {
    return &productRepository{db: db}
}

func (r *productRepository) FindByID(id int) (*model.Product, error) {
    p := &model.Product{}
    err := r.db.QueryRow("SELECT id, name, description, price, stock FROM products WHERE id = ?", id).
        Scan(&p.ID, &p.Name, &p.Description, &p.Price, &p.Stock)
    if err == sql.ErrNoRows {
        return nil, nil
    }
    return p, err
}

func (r *productRepository) List() ([]*model.Product, error) {
    rows, err := r.db.Query("SELECT id, name, description, price, stock FROM products")
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var products []*model.Product
    for rows.Next() {
        p := &model.Product{}
        if err := rows.Scan(&p.ID, &p.Name, &p.Description, &p.Price, &p.Stock); err != nil {
            return nil, err
        }
        products = append(products, p)
    }
    return products, rows.Err()
}

func (r *productRepository) Create(product *model.Product) error {
    result, err := r.db.Exec(
        "INSERT INTO products (name, description, price, stock) VALUES (?, ?, ?, ?)",
        product.Name, product.Description, product.Price, product.Stock,
    )
    if err != nil {
        return err
    }
    id, _ := result.LastInsertId()
    product.ID = int(id)
    return nil
}

func (r *productRepository) UpdateStock(id int, stock int) error {
    _, err := r.db.Exec("UPDATE products SET stock = ? WHERE id = ?", stock, id)
    return err
}
GO
// internal/repository/order.go
package repository

import (
    "database/sql"
    "ecommerce/pkg/model"
)

type OrderRepository interface {
    FindByID(id int) (*model.Order, error)
    ListByUserID(userID int) ([]*model.Order, error)
    Create(order *model.Order) error
}

type orderRepository struct {
    db *sql.DB
}

func NewOrderRepository(db *sql.DB) OrderRepository {
    return &orderRepository{db: db}
}

func (r *orderRepository) FindByID(id int) (*model.Order, error) {
    o := &model.Order{}
    err := r.db.QueryRow(
        "SELECT id, user_id, product_id, quantity, total_price, status FROM orders WHERE id = ?", id,
    ).Scan(&o.ID, &o.UserID, &o.ProductID, &o.Quantity, &o.TotalPrice, &o.Status)
    if err == sql.ErrNoRows {
        return nil, nil
    }
    return o, err
}

func (r *orderRepository) ListByUserID(userID int) ([]*model.Order, error) {
    rows, err := r.db.Query(
        "SELECT id, user_id, product_id, quantity, total_price, status FROM orders WHERE user_id = ?",
        userID,
    )
    if err != nil {
        return nil, err
    }
    defer rows.Close()

    var orders []*model.Order
    for rows.Next() {
        o := &model.Order{}
        if err := rows.Scan(&o.ID, &o.UserID, &o.ProductID, &o.Quantity, &o.TotalPrice, &o.Status); err != nil {
            return nil, err
        }
        orders = append(orders, o)
    }
    return orders, rows.Err()
}

func (r *orderRepository) Create(order *model.Order) error {
    result, err := r.db.Exec(
        "INSERT INTO orders (user_id, product_id, quantity, total_price, status) VALUES (?, ?, ?, ?, ?)",
        order.UserID, order.ProductID, order.Quantity, order.TotalPrice, order.Status,
    )
    if err != nil {
        return err
    }
    id, _ := result.LastInsertId()
    order.ID = int(id)
    return nil
}

(3) طبقة الخدمة

GO
// internal/service/errors.go
package service

import "errors"

var (
    ErrEmailTaken      = errors.New("email already registered")
    ErrInvalidCreds    = errors.New("invalid email or password")
    ErrProductNotFound = errors.New("product not found")
    ErrInsufficientStock = errors.New("insufficient stock")
    ErrOrderNotFound   = errors.New("الترتيب not found")
    ErrUnauthorized    = errors.New("unauthorized")
)
GO
// internal/service/user.go
package service

import (
    "ecommerce/internal/repository"
    "ecommerce/pkg/model"
    "strings"
)

type UserService struct {
    userRepo repository.UserRepository
}

func NewUserService(userRepo repository.UserRepository) *UserService {
    return &UserService{userRepo: userRepo}
}

func (s *UserService) Register(req model.RegisterRequest) (*model.User, error) {
    if strings.TrimSpace(req.Email) == "" {
        return nil, errors.New("email is required")
    }
    if strings.TrimSpace(req.Password) == "" {
        return nil, errors.New("password is required")
    }
    if strings.TrimSpace(req.Name) == "" {
        return nil, errors.New("name is required")
    }

    existing, _ := s.userRepo.FindByEmail(req.Email)
    if existing != nil {
        return nil, ErrEmailTaken
    }

    user := &model.User{
        Email:    req.Email,
        Password: req.Password, // Use bcrypt in production
        Name:     req.Name,
    }

    if err := s.userRepo.Create(user); err != nil {
        return nil, err
    }

    return user, nil
}

func (s *UserService) Login(req model.LoginRequest) (*model.User, error) {
    user, err := s.userRepo.FindByEmail(req.Email)
    if err != nil {
        return nil, err
    }
    if user == nil || user.Password != req.Password {
        return nil, ErrInvalidCreds
    }
    return user, nil
}

func (s *UserService) GetUserByID(id int) (*model.User, error) {
    return s.userRepo.FindByID(id)
}
GO
// internal/service/product.go
package service

import (
    "ecommerce/internal/repository"
    "ecommerce/pkg/model"
)

type ProductService struct {
    productRepo repository.ProductRepository
}

func NewProductService(productRepo repository.ProductRepository) *ProductService {
    return &ProductService{productRepo: productRepo}
}

func (s *ProductService) ListProducts() ([]*model.Product, error) {
    return s.productRepo.List()
}

func (s *ProductService) GetProduct(id int) (*model.Product, error) {
    product, err := s.productRepo.FindByID(id)
    if err != nil {
        return nil, err
    }
    if product == nil {
        return nil, ErrProductNotFound
    }
    return product, nil
}
GO
// internal/service/الترتيب.go
package service

import (
    "ecommerce/internal/repository"
    "ecommerce/pkg/model"
)

type OrderService struct {
    orderRepo   repository.OrderRepository
    productRepo repository.ProductRepository
    userRepo    repository.UserRepository
}

func NewOrderService(
    orderRepo repository.OrderRepository,
    productRepo repository.ProductRepository,
    userRepo repository.UserRepository,
) *OrderService {
    return &OrderService{
        orderRepo:   orderRepo,
        productRepo: productRepo,
        userRepo:    userRepo,
    }
}

func (s *OrderService) CreateOrder(userID int, req model.CreateOrderRequest) (*model.Order, error) {
    if req.Quantity <= 0 {
        return nil, errors.New("quantity must be positive")
    }

    product, err := s.productRepo.FindByID(req.ProductID)
    if err != nil {
        return nil, err
    }
    if product == nil {
        return nil, ErrProductNotFound
    }
    if product.Stock < req.Quantity {
        return nil, ErrInsufficientStock
    }

    totalPrice := product.Price * float64(req.Quantity)

    الترتيب := &model.Order{
        UserID:     userID,
        ProductID:  req.ProductID,
        Quantity:   req.Quantity,
        TotalPrice: totalPrice,
        Status:     "pending",
    }

    if err := s.orderRepo.Create(الترتيب); err != nil {
        return nil, err
    }

    // Deduct stock
    newStock := product.Stock - req.Quantity
    if err := s.productRepo.UpdateStock(product.ID, newStock); err != nil {
        return nil, err
    }

    return الترتيب, nil
}

func (s *OrderService) GetOrder(orderID, userID int) (*model.Order, error) {
    الترتيب, err := s.orderRepo.FindByID(orderID)
    if err != nil {
        return nil, err
    }
    if الترتيب == nil {
        return nil, ErrOrderNotFound
    }
    if الترتيب.UserID != userID {
        return nil, ErrUnauthorized
    }
    return الترتيب, nil
}

func (s *OrderService) ListUserOrders(userID int) ([]*model.Order, error) {
    return s.orderRepo.ListByUserID(userID)
}

(4) طبقة المعالج

GO
// internal/handler/response.go
package handler

import (
    "encoding/json"
    "net/http"
)

type APIResponse struct {
    Data  interface{} `json:"data,omitempty"`
    Error string      `json:"error,omitempty"`
}

func writeJSON(w http.ResponseWriter, status int, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(APIResponse{Data: data})
}

func writeError(w http.ResponseWriter, status int, msg string) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(APIResponse{Error: msg})
}
GO
// internal/handler/user.go
package handler

import (
    "encoding/json"
    "errors"
    "net/http"
    "ecommerce/internal/service"
    "ecommerce/pkg/model"
)

type UserHandler struct {
    userSvc *service.UserService
}

func NewUserHandler(userSvc *service.UserService) *UserHandler {
    return &UserHandler{userSvc: userSvc}
}

func (h *UserHandler) Register(mux *http.ServeMux) {
    mux.HandleFunc("POST /api/v1/register", h.RegisterUser)
    mux.HandleFunc("POST /api/v1/login", h.LoginUser)
}

func (h *UserHandler) RegisterUser(w http.ResponseWriter, r *http.Request) {
    var req model.RegisterRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        writeError(w, http.StatusBadRequest, "invalid JSON")
        return
    }

    user, err := h.userSvc.Register(req)
    if errors.Is(err, service.ErrEmailTaken) {
        writeError(w, http.StatusConflict, err.Error())
        return
    }
    if err != nil {
        writeError(w, http.StatusBadRequest, err.Error())
        return
    }

    writeJSON(w, http.StatusCreated, user)
}

func (h *UserHandler) LoginUser(w http.ResponseWriter, r *http.Request) {
    var req model.LoginRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        writeError(w, http.StatusBadRequest, "invalid JSON")
        return
    }

    user, err := h.userSvc.Login(req)
    if errors.Is(err, service.ErrInvalidCreds) {
        writeError(w, http.StatusUnauthorized, err.Error())
        return
    }
    if err != nil {
        writeError(w, http.StatusInternalServerError, "login failed")
        return
    }

    writeJSON(w, http.StatusOK, user)
}
GO
// internal/handler/product.go
package handler

import (
    "errors"
    "net/http"
    "strconv"
    "ecommerce/internal/service"
)

type ProductHandler struct {
    productSvc *service.ProductService
}

func NewProductHandler(productSvc *service.ProductService) *ProductHandler {
    return &ProductHandler{productSvc: productSvc}
}

func (h *ProductHandler) Register(mux *http.ServeMux) {
    mux.HandleFunc("GET /api/v1/products", h.ListProducts)
    mux.HandleFunc("GET /api/v1/products/{id}", h.GetProduct)
}

func (h *ProductHandler) ListProducts(w http.ResponseWriter, r *http.Request) {
    products, err := h.productSvc.ListProducts()
    if err != nil {
        writeError(w, http.StatusInternalServerError, "failed to list products")
        return
    }
    writeJSON(w, http.StatusOK, products)
}

func (h *ProductHandler) GetProduct(w http.ResponseWriter, r *http.Request) {
    id, err := strconv.Atoi(r.PathValue("id"))
    if err != nil {
        writeError(w, http.StatusBadRequest, "invalid product ID")
        return
    }

    product, err := h.productSvc.GetProduct(id)
    if errors.Is(err, service.ErrProductNotFound) {
        writeError(w, http.StatusNotFound, err.Error())
        return
    }
    if err != nil {
        writeError(w, http.StatusInternalServerError, "failed to get product")
        return
    }

    writeJSON(w, http.StatusOK, product)
}
GO
// internal/handler/order.go
package handler

import (
    "encoding/json"
    "errors"
    "net/http"
    "strconv"
    "ecommerce/internal/service"
    "ecommerce/pkg/model"
)

type OrderHandler struct {
    orderSvc *service.OrderService
}

func NewOrderHandler(orderSvc *service.OrderService) *OrderHandler {
    return &OrderHandler{orderSvc: orderSvc}
}

func (h *OrderHandler) Register(mux *http.ServeMux) {
    mux.HandleFunc("POST /api/v1/orders", h.CreateOrder)
    mux.HandleFunc("GET /api/v1/orders", h.ListOrders)
    mux.HandleFunc("GET /api/v1/orders/{id}", h.GetOrder)
}

// Simulate getting userID from Context (JWT implemented in Part 2)
func getUserID(r *http.Request) int {
    if id, ok := r.Context().Value("user_id").(int); ok {
        return id
    }
    return 1 // Default user (temporary for Part 1)
}

func (h *OrderHandler) CreateOrder(w http.ResponseWriter, r *http.Request) {
    userID := getUserID(r)

    var req model.CreateOrderRequest
    if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
        writeError(w, http.StatusBadRequest, "invalid JSON")
        return
    }

    order, err := h.orderSvc.CreateOrder(userID, req)
    if errors.Is(err, service.ErrProductNotFound) {
        writeError(w, http.StatusNotFound, err.Error())
        return
    }
    if errors.Is(err, service.ErrInsufficientStock) {
        writeError(w, http.StatusConflict, err.Error())
        return
    }
    if err != nil {
        writeError(w, http.StatusBadRequest, err.Error())
        return
    }

    writeJSON(w, http.StatusCreated, order)
}

func (h *OrderHandler) GetOrder(w http.ResponseWriter, r *http.Request) {
    userID := getUserID(r)
    id, err := strconv.Atoi(r.PathValue("id"))
    if err != nil {
        writeError(w, http.StatusBadRequest, "invalid order ID")
        return
    }

    order, err := h.orderSvc.GetOrder(id, userID)
    if errors.Is(err, service.ErrOrderNotFound) {
        writeError(w, http.StatusNotFound, err.Error())
        return
    }
    if errors.Is(err, service.ErrUnauthorized) {
        writeError(w, http.StatusForbidden, err.Error())
        return
    }
    if err != nil {
        writeError(w, http.StatusInternalServerError, "failed to get order")
        return
    }

    writeJSON(w, http.StatusOK, order)
}

func (h *OrderHandler) ListOrders(w http.ResponseWriter, r *http.Request) {
    userID := getUserID(r)
    orders, err := h.orderSvc.ListUserOrders(userID)
    if err != nil {
        writeError(w, http.StatusInternalServerError, "failed to list orders")
        return
    }
    writeJSON(w, http.StatusOK, orders)
}

(5) الرئيسي (حقن التبعية)

⚙️ المتطلبات الأساسية: قم بتشغيل go get github.com/mattn/go-sqlite3 (يتطلب CGO؛ أو استخدم modernc.org/sqlite كبديل للحصول على برنامج تشغيل مخصص للعبة «غو» فقط)

GO
package main

import (
    "database/sql"
    "log"
    "net/http"

    "ecommerce/internal/handler"
    "ecommerce/internal/repository"
    "ecommerce/internal/service"

    _ "github.com/mattn/go-sqlite3"
)

func main() {
    db, err := sql.Open("sqlite3", "./ecommerce.db")
    if err != nil {
        log.Fatal(err)
    }
    defer db.Close()

    initDB(db)

    // Dependency injection
    userRepo := repository.NewUserRepository(db)
    productRepo := repository.NewProductRepository(db)
    orderRepo := repository.NewOrderRepository(db)

    userSvc := service.NewUserService(userRepo)
    productSvc := service.NewProductService(productRepo)
    orderSvc := service.NewOrderService(orderRepo, productRepo, userRepo)

    userHandler := handler.NewUserHandler(userSvc)
    productHandler := handler.NewProductHandler(productSvc)
    orderHandler := handler.NewOrderHandler(orderSvc)

    mux := http.NewServeMux()
    userHandler.Register(mux)
    productHandler.Register(mux)
    orderHandler.Register(mux)

    log.Println("E-commerce API listening on :8080")
    log.Fatal(http.ListenAndServe(":8080", mux))
}

func initDB(db *sql.DB) {
    schema := `
    CREATE TABLE IF NOT EXISTS users (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        email TEXT UNIQUE NOT NULL,
        password TEXT NOT NULL,
        name TEXT NOT NULL
    );
    CREATE TABLE IF NOT EXISTS products (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT NOT NULL,
        description TEXT,
        price REAL NOT NULL,
        stock INTEGER NOT NULL DEFAULT 0
    );
    CREATE TABLE IF NOT EXISTS orders (
        id INTEGER PRIMARY KEY AUTOINCREMENT,
        user_id INTEGER NOT NULL,
        product_id INTEGER NOT NULL,
        quantity INTEGER NOT NULL,
        total_price REAL NOT NULL,
        status TEXT NOT NULL DEFAULT 'pending',
        FOREIGN KEY (user_id) REFERENCES users(id),
        FOREIGN KEY (product_id) REFERENCES products(id)
    );`
    if _, err := db.Exec(schema); err != nil {
        log.Fatal("initDB:", err)
    }
}

(6) الاختبار الفردي (مستودع النماذج الوهمية)

GO
// internal/service/order_test.go
package service

import (
    "testing"
    "ecommerce/pkg/model"
)

// mock Repository
type mockProductRepo struct {
    products map[int]*model.Product
}

func (m *mockProductRepo) FindByID(id int) (*model.Product, error) {
    return m.products[id], nil
}

func (m *mockProductRepo) List() ([]*model.Product, error) {
    var result []*model.Product
    for _, p := range m.products {
        result = append(result, p)
    }
    return result, nil
}

func (m *mockProductRepo) Create(product *model.Product) error {
    product.ID = len(m.products) + 1
    m.products[product.ID] = product
    return nil
}

func (m *mockProductRepo) UpdateStock(id int, stock int) error {
    if p, ok := m.products[id]; ok {
        p.Stock = stock
    }
    return nil
}

type mockOrderRepo struct {
    orders []*model.Order
}

func (m *mockOrderRepo) FindByID(id int) (*model.Order, error) {
    for _, o := range m.orders {
        if o.ID == id {
            return o, nil
        }
    }
    return nil, nil
}

func (m *mockOrderRepo) ListByUserID(userID int) ([]*model.Order, error) {
    var result []*model.Order
    for _, o := range m.orders {
        if o.UserID == userID {
            result = append(result, o)
        }
    }
    return result, nil
}

func (m *mockOrderRepo) Create(الترتيب *model.Order) error {
    الترتيب.ID = len(m.orders) + 1
    m.orders = append(m.orders, الترتيب)
    return nil
}

func TestCreateOrder_Success(t *testing.T) {
    productRepo := &mockProductRepo{
        products: map[int]*model.Product{
            1: {ID: 1, Name: "Laptop", Price: 999.99, Stock: 10},
        },
    }
    orderRepo := &mockOrderRepo{}
    svc := NewOrderService(orderRepo, productRepo, nil)

    الترتيب, err := svc.CreateOrder(1, model.CreateOrderRequest{ProductID: 1, Quantity: 2})
    if err != nil {
        t.Fatalf("expected no error, got %v", err)
    }
    if الترتيب.TotalPrice != 1999.98 {
        t.Errorf("expected total 1999.98, got %.2f", الترتيب.TotalPrice)
    }
}

func TestCreateOrder_InsufficientStock(t *testing.T) {
    productRepo := &mockProductRepo{
        products: map[int]*model.Product{
            1: {ID: 1, Name: "Laptop", Price: 999.99, Stock: 1},
        },
    }
    orderRepo := &mockOrderRepo{}
    svc := NewOrderService(orderRepo, productRepo, nil)

    _, err := svc.CreateOrder(1, model.CreateOrderRequest{ProductID: 1, Quantity: 5})
    if err != ErrInsufficientStock {
        t.Errorf("expected ErrInsufficientStock, got %v", err)
    }
}

func TestCreateOrder_ProductNotFound(t *testing.T) {
    productRepo := &mockProductRepo{products: make(map[int]*model.Product)}
    orderRepo := &mockOrderRepo{}
    svc := NewOrderService(orderRepo, productRepo, nil)

    _, err := svc.CreateOrder(1, model.CreateOrderRequest{ProductID: 999, Quantity: 1})
    if err != ErrProductNotFound {
        t.Errorf("expected ErrProductNotFound, got %v", err)
    }
}
💡 نصيحة: من خلال تعريف طبقة المستودع باستخدام واجهة (مثل type UserRepository interface{...})، يمكنك إدخال تطبيقات وهمية في اختباراتك. وهذه ميزة كبيرة للواجهات الضمنية في لغة Go — فأنت لا تحتاج إلى إطار عمل للمحاكاة؛ بل يمكنك ببساطة كتابة نماذج وهمية خفيفة الوزن يدويًّا.



❓ أسئلة شائعة

س كيف يتم تطبيق النموذج ذي الطبقات الأربع في المشاريع العملية؟
ج ابدأ بالنموذج (بنية البيانات) → المستودع (عمليات CRUD لقاعدة البيانات) → الخدمة (قواعد العمل) → المعالج (HTTP). يجب ألا يتجاوز حجم كل ملف 200 سطر. يتم تنظيم الملفات حسب المورد (المستخدم/المنتج/الطلب). وتتولى الدالة الرئيسية مسؤولية تجميع جميع التبعيات (حقن التبعيات).
س كيف ينبغي تصميم الجداول الوهمية لاختبار الوحدات؟
ج قم بتعريف «المستودع» (Repository) باستخدام واجهة، وقم بتمرير تنفيذ وهمي أثناء الاختبار. قم بتخزين البيانات الوهمية في الذاكرة باستخدام خريطة (map). قم بإنشاء نسخة وهمية من «المستودع» عند اختبار طبقة الخدمة (Service)، وقم بإنشاء نسخة وهمية من «الخدمة» عند اختبار طبقة المعالج (Handler). لا حاجة إلى إطار عمل خاص بالنسخ الوهمية — فالواجهات الضمنية في لغة Go تجعل عملية إنشاء النسخ الوهمية يدويًّا عملية خفيفة للغاية.
س ما هي الأنماط الثلاثة لمعالجة الأخطاء؟
ج (1) أنواع الأخطاء المخصصة (بما في ذلك Code/Message/HTTPStatus) — مناسبة للمشاريع الكبيرة؛ (2) أخطاء الحراسة (var ErrXxx = errors.New(...)) — مناسبة للمشاريع المتوسطة الحجم؛ (3) أخطاء السلسلة — مناسبة فقط للمشاريع الصغيرة. يستخدم هذا المشروع أخطاء الحراسة.
س ما هو تصميم الموارد وفقًا لمعيار RESTful؟
ج المستخدمون (التسجيل/تسجيل الدخول)، المنتجات (القائمة/التفاصيل)، الطلبات (الإنشاء/القائمة/التفاصيل). لكل مورد معالج وخدمة ومستودع خاص به. تستخدم عناوين URL أسماءً بصيغة الجمع: /api/v1/users، /api/v1/products، /api/v1/orders.
س مقارنة بين مكتبات التحقق من الصحة؟
ج (1) التحقق اليدوي — في هذا المشروع، نستخدم strings.TrimSpace للتحقق — بدون أي تبعيات؛ (2) go-playground/validator — التحقق القائم على العلامات، وهو أداة قوية جدًّا؛ (3) بنية Validator المخصصة. بالنسبة للمشاريع الصغيرة، يكفي التحقق اليدوي.

📖 ملخص


📝 تمارين

  1. الأساسي (مستوى الصعوبة ⭐): قم بتشغيل الكود الكامل لهذا الدرس واستخدم curl لاختبار نقاط النهاية مثل التسجيل، وتسجيل الدخول، وقائمة المنتجات، وإنشاء الطلبات. تأكد من أن جميع عمليات CRUD تعمل بشكل صحيح.

  2. متقدم (درجة الصعوبة ⭐⭐): أضف اختبارات وحدة كاملة إلى طبقة الخدمات في النظام. المتطلبات: (1) تغطية سيناريو تكرار البريد الإلكتروني في UserService.Register؛ (2) تغطية سيناريو «البريد الإلكتروني غير موجود» في ProductService.GetProduct؛ (3) تغطية سيناريو «غير مصرح به» في OrderService.GetOrder؛ (4) تغطية الاختبار > 80%.

  3. التحدي (الصعوبة ⭐⭐⭐): أضف مورد «الفئة» إلى النظام. المتطلبات: (1) أضف حقل «CategoryID» إلى نموذج «المنتج»؛ (2) قم بتنفيذ عمليات CRUD للفئات (وظائف المسؤول)؛ (3) تنفيذ نقطة النهاية GET /products?category_id=X للتصفية حسب الفئة؛ (4) التحقق من وجود الفئات في طبقة الخدمة؛ (5) توفير اختبارات وحدة كاملة.

Web-Tutorial.com

فريق Web-Tutorial التقني

منصة دروس برمجية يديرها عدة مطورين. كل درس يتم كتابته ومراجعته بواسطة مطورين متخصصين في المجال. نعمل على ضمان دقة وموثوقية المحتوى — إذا لاحظت أي مشكلة، فيرجى إخبارنا.

100%