Go: مشروع شامل: واجهة برمجة تطبيقات التجارة الإلكترونية…
آخر تحديث: 2026-08-26
هذا كل ما يتعلق بالنظرية. والآن لنبدأ — في بناء واجهة برمجة تطبيقات (API) حقيقية للتجارة الإلكترونية من الصفر باستخدام لغة Go.
قرر بوب إنشاء منصة التجارة الإلكترونية الخاصة به. ثلاثة أسابيع، ثلاث قصص، ونظام خلفي كامل مكتوب بلغة Go.
1. ستتعلم
- تحليل متطلبات واجهة برمجة التطبيقات (API) للتجارة الإلكترونية وتصميم بنيتها
- التنفيذ الكامل للبنية المكونة من 4 طبقات: النموذج → المستودع → الخدمة → المعالج
- نقاط نهاية CRUD + التحقق من صحة المعلمات
- أنواع الأخطاء المخصصة + معالجة موحدة للأخطاء
- اختبار الوحدات (مستودع النماذج الوهمية)
2. القصة: اليوم الأول لبوب كرائد أعمال
(1) المشكلة: «سأقوم بتطوير نموذج قابل للتطبيق (MVP) أولاً وأطلقه في غضون أسبوعين.»
استقال بوب من وظيفته لبدء مشروعه الخاص — منصة للتجارة الإلكترونية:
"في اليوم الأول، قمت بإنشاء برنامج متكامل — تضمن كل المنطق في
main.go. وبحلول اليوم السابع، كان حجم الكود قد نما إلى 3,000 سطر، ولم أعد أستطيع التمييز بين الأجزاء التي تتعلق بمنطق التسلسل والأجزاء التي تتعلق بمنطق المستخدم. أردت إضافة ميزة «القسيمة»، لكن بعد تعديل ستة ملفات، لم أكن قد انتهيت بعد. قال شريكي في العمل: «أطلق المشروع في غضون أسبوعين»، لكنني أعتقد أن حتى شهرين قد يكونان فترة طويلة جدًا."
ولكن بعد أن هدأ وتأمل في بنية «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 للموارد الأساسية الثلاثة.
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. هيكل المشروع
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. التنفيذ الكامل
▶ مثال: طبقة النموذج
// 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"`
}
// 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"`
}
// 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"`
}
▶ مثال: طبقة المستودع
// 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
}
// 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
}
// 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) طبقة الخدمة
// 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")
)
// 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)
}
// 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
}
// 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) طبقة المعالج
// 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})
}
// 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)
}
// 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)
}
// 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كبديل للحصول على برنامج تشغيل مخصص للعبة «غو» فقط)
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) الاختبار الفردي (مستودع النماذج الوهمية)
// 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 — فأنت لا تحتاج إلى إطار عمل للمحاكاة؛ بل يمكنك ببساطة كتابة نماذج وهمية خفيفة الوزن يدويًّا.
❓ أسئلة شائعة
var ErrXxx = errors.New(...)) — مناسبة للمشاريع المتوسطة الحجم؛ (3) أخطاء السلسلة — مناسبة فقط للمشاريع الصغيرة. يستخدم هذا المشروع أخطاء الحراسة./api/v1/users، /api/v1/products، /api/v1/orders.strings.TrimSpace للتحقق — بدون أي تبعيات؛ (2) go-playground/validator — التحقق القائم على العلامات، وهو أداة قوية جدًّا؛ (3) بنية Validator المخصصة. بالنسبة للمشاريع الصغيرة، يكفي التحقق اليدوي.📖 ملخص
- إطار عمل واجهة برمجة تطبيقات التجارة الإلكترونية: ثلاثة موارد أساسية — المستخدمون، والمنتجات، والطلبات
- بنية من 4 طبقات: النموذج → المستودع → الخدمة → المعالج
- واجهة المستودع: تسهل إجراء الاختبارات الوهمية
- قواعد أعمال الخدمة: التحقق من الصحة → الاستعلام → الحساب → الكتابة
- معالج HTTP: تحليل JSON + تعيين الأخطاء + الردود الموحدة
- اختبار الوحدة: محاكاة المستودع لاختبار منطق الأعمال الخاص بالخدمة
- ملف الإدخال
main: حقن التبعية لتجميع جميع الطبقات
📝 تمارين
-
الأساسي (مستوى الصعوبة ⭐): قم بتشغيل الكود الكامل لهذا الدرس واستخدم
curlلاختبار نقاط النهاية مثل التسجيل، وتسجيل الدخول، وقائمة المنتجات، وإنشاء الطلبات. تأكد من أن جميع عمليات CRUD تعمل بشكل صحيح. -
متقدم (درجة الصعوبة ⭐⭐): أضف اختبارات وحدة كاملة إلى طبقة الخدمات في النظام. المتطلبات: (1) تغطية سيناريو تكرار البريد الإلكتروني في UserService.Register؛ (2) تغطية سيناريو «البريد الإلكتروني غير موجود» في
ProductService.GetProduct؛ (3) تغطية سيناريو «غير مصرح به» فيOrderService.GetOrder؛ (4) تغطية الاختبار > 80%. -
التحدي (الصعوبة ⭐⭐⭐): أضف مورد «الفئة» إلى النظام. المتطلبات: (1) أضف حقل «CategoryID» إلى نموذج «المنتج»؛ (2) قم بتنفيذ عمليات CRUD للفئات (وظائف المسؤول)؛ (3) تنفيذ نقطة النهاية GET /products?category_id=X للتصفية حسب الفئة؛ (4) التحقق من وجود الفئات في طبقة الخدمة؛ (5) توفير اختبارات وحدة كاملة.