404 Not Found

404 Not Found


nginx

تمرين عملي شامل: تطوير مشروع OrderFlow

تطوير المشروع هو عملية تحويل التصميم إلى كود—التقدم بطريقة معيارية، مع كل خطوة قابلة للتحقق والتكامل المستمر لمنع تراكم العمل.

1. ما ستتعلمه


2. قصة حقيقية لمطور Full-Stack

(1) نقطة الألم: الفجوة بين التصميم والكود

أكملت Alice تصميم المنظومة لـ OrderFlow، لكنها تشعر بالإرهاق من حجم الكود الهائل—5 جداول، 20 واجهة برمجة تطبيقات، و4 وحدات. هل تبدأ بكتابة كود قاعدة البيانات أم كود واجهة برمجة التطبيقات؟ كيف يجب أن تعتمد الوحدات على بعضها البعض؟ كيف يمكنها التأكد من أن كل خطوة تعمل وتجتاز التحقق؟

(2) حلول التطوير المعياري

تقدم خطوة بخطوة على أساس الوحدات، مع كل وحدة قابلة للتحقق بشكل مستقل:

100%
graph TD
    A["وحدة المستخدمين<br/>المصادقة + JWT"] --> B["وحدة المنتجات<br/>CRUD + تخزين مؤقت"]
    B --> C["وحدة الطلبات<br/>معاملات + جدولة"]
    C --> D["وحدة المدفوعات<br/>رد الاتصال + ترابطية"]
    D --> E["المقاطع العرضية<br/>استثناءات + تسجيل + توثيق"]

(3) الإيرادات

طورت Alice المشروع وحدة تلو الأخرى، مشغّلة الاختبارات للتحقق من كل وحدة عند إكمالها. أنهت الكود بالكامل في غضون أسبوعين، متجنبة مشاكل "الانفجار الكبير" المرتبطة بالتكامل لمرة واحدة.


3. وحدة المستخدمين

(1) ▶ مثال: كيان المستخدم والمستودع

JAVA
@Entity
@Table(name = "users")
public class User {
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false, unique = true, length = 50)
    private String username;

    @Column(nullable = false, unique = true, length = 100)
    private String email;

    @Column(nullable = false, name = "password_hash")
    private String passwordHash;

    @Column(nullable = false, length = 20)
    private String role = "CUSTOMER";

    @CreationTimestamp
    private Instant createdAt;

    protected User() {}

    public User(String username, String email, String passwordHash, String role) {
        this.username = username;
        this.email = email;
        this.passwordHash = passwordHash;
        this.role = role;
    }
    // getters
}

public interface UserRepository extends JpaRepository<User, Long> {
    Optional<User> findByUsername(String username);
    boolean existsByUsername(String username);
    boolean existsByEmail(String email);
}

الناتج:

TEXT
// التنفيذ ناجح

(2) ▶ مثال: التسجيل وتسجيل الدخول

JAVA
@Service
public class AuthService {

    private final UserRepository userRepository;
    private final PasswordEncoder passwordEncoder;
    private final JwtEncoder jwtEncoder;

    public AuthService(UserRepository userRepository,
                       PasswordEncoder passwordEncoder,
                       JwtEncoder jwtEncoder) {
        this.userRepository = userRepository;
        this.passwordEncoder = passwordEncoder;
        this.jwtEncoder = jwtEncoder;
    }

    @Transactional
    public UserResponse register(RegisterRequest request) {
        if (userRepository.existsByUsername(request.username())) {
            throw new BusinessException("USERNAME_EXISTS", "اسم المستخدم مستخدم بالفعل");
        }
        User user = new User(
            request.username(),
            request.email(),
            passwordEncoder.encode(request.password()),
            "CUSTOMER"
        );
        return UserResponse.from(userRepository.save(user));
    }

    public Map<String, String> login(LoginRequest request) {
        Authentication auth = authenticationManager.authenticate(
            new UsernamePasswordAuthenticationToken(
                request.username(), request.password()));
        Instant now = Instant.now();
        JwtClaimsSet claims = JwtClaimsSet.builder()
            .issuer("orderflow").subject(auth.getName())
            .issuedAt(now).expiresAt(now.plus(2, ChronoUnit.HOURS))
            .claim("role", extractRole(auth))
            .build();
        String token = jwtEncoder
            .encode(JwtEncoderParameters.from(claims)).getTokenValue();
        return Map.of("accessToken", token);
    }
}

الناتج:

TEXT
// التنفيذ ناجح

4. وحدة المنتجات

(1) ▶ مثال: ProductService مع التخزين المؤقت

JAVA
@Service
public class ProductService {

    private final ProductRepository productRepository;
    private final ProductSearchRepository searchRepository; // Redis

    @Cacheable(value = "products", key = "#id")
    public ProductResponse getProduct(Long id) {
        Product product = productRepository.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("Product", id));
        return ProductResponse.from(product);
    }

    @Cacheable(value = "product-list",
        key = "#keyword + '-' + #pageable.pageNumber + '-' + #pageable.pageSize")
    public PagedResponse<ProductResponse> searchProducts(
            String keyword, Pageable pageable) {
        Page<Product> page = productRepository
            .findByNameContaining(keyword, pageable);
        return PagedResponse.from(page.map(ProductResponse::from));
    }

    @CacheEvict(value = "product-list", allEntries = true)
    @CachePut(value = "products", key = "#result.id()")
    public ProductResponse createProduct(CreateProductRequest request) {
        Product product = new Product(
            request.name(), request.sku(),
            request.price(), request.stock(), request.category());
        return ProductResponse.from(productRepository.save(product));
    }

    @CachePut(value = "products", key = "#result.id()")
    @CacheEvict(value = "product-list", allEntries = true)
    public ProductResponse updateProduct(Long id, UpdateProductRequest request) {
        Product product = productRepository.findById(id)
            .orElseThrow(() -> new ResourceNotFoundException("Product", id));
        product.setName(request.name());
        product.setPrice(request.price());
        product.setStock(request.stock());
        return ProductResponse.from(productRepository.save(product));
    }

    @Caching(evict = {
        @CacheEvict(value = "products", key = "#id"),
        @CacheEvict(value = "product-list", allEntries = true)
    })
    public void deleteProduct(Long id) {
        productRepository.deleteById(id);
    }
}

الناتج:

TEXT
// التنفيذ ناجح

5. وحدة الطلبات

(1) ▶ مثال: معاملة طلب OrderService

JAVA
@Service
public class OrderService {

    private final OrderRepository orderRepository;
    private final ProductRepository productRepository;
    private final UserRepository userRepository;
    private final OrderEventPublisher eventPublisher;

    @Transactional(rollbackFor = Exception.class)
    public OrderResponse createOrder(String username, CreateOrderRequest request) {
        User user = userRepository.findByUsername(username)
            .orElseThrow(() -> new ResourceNotFoundException("User", 0L));

        Product product = productRepository.findById(request.productId())
            .orElseThrow(() -> new ResourceNotFoundException("Product", request.productId()));

        if (product.getStock() < request.quantity()) {
            throw new InsufficientStockException(
                product.getId(), product.getStock(), request.quantity());
        }

        product.deductStock(request.quantity());
        Order order = new Order(user, product, request.quantity());
        Order saved = orderRepository.save(order);

        eventPublisher.publishOrderCreated(saved.getId());
        return OrderResponse.from(saved);
    }

    @Transactional(rollbackFor = Exception.class)
    public void cancelOrder(String username, Long orderId) {
        Order order = orderRepository.findByIdWithItems(orderId)
            .orElseThrow(() -> new ResourceNotFoundException("Order", orderId));

        if (!"PENDING".equals(order.getStatus())) {
            throw new OrderStateException(orderId, order.getStatus(), "CANCELLED");
        }

        order.getItems().forEach(item ->
            item.getProduct().addStock(item.getQuantity()));
        order.setStatus("CANCELLED");

        eventPublisher.publishOrderCancelled(orderId);
    }

    @Scheduled(fixedRateString = "${orderflow.order.expiry-check-interval:300000}")
    @Transactional
    public void cancelExpiredOrders() {
        List<Order> expired = orderRepository
            .findByStatusAndCreatedAtBefore("PENDING",
                Instant.now().minus(30, ChronoUnit.MINUTES));
        expired.forEach(order -> {
            order.getItems().forEach(item ->
                item.getProduct().addStock(item.getQuantity()));
            order.setStatus("CANCELLED");
        });
        if (!expired.isEmpty()) {
            log.info("تم إلغاء {} طلبات منتهية الصلاحية تلقائيًا", expired.size());
        }
    }
}

الناتج:

TEXT
// التنفيذ ناجح

6. وحدة المدفوعات

(1) ▶ مثال: PaymentService مع الترابطية

JAVA
@Service
public class PaymentService {

    private final PaymentRepository paymentRepository;
    private final OrderRepository orderRepository;
    private final StringRedisTemplate redis;

    @Transactional(rollbackFor = Exception.class)
    public PaymentResponse initiatePayment(String username, CreatePaymentRequest request) {
        Order order = orderRepository.findById(request.orderId())
            .orElseThrow(() -> new ResourceNotFoundException("Order", request.orderId()));

        if (!"PENDING".equals(order.getStatus())) {
            throw new OrderStateException(order.getId(), order.getStatus(), "PAYMENT");
        }

        // فحص الترابطية: نفس transaction_id يجب ألا ينشئ دفعًا مكررًا
        String idempotencyKey = "payment:idempotent:" + request.orderId();
        Boolean isNew = redis.opsForValue()
            .setIfAbsent(idempotencyKey, "1", Duration.ofMinutes(10));
        if (Boolean.FALSE.equals(isNew)) {
            throw new BusinessException("DUPLICATE_PAYMENT",
                "تم بدء الدفع بالفعل للطلب " + request.orderId());
        }

        Payment payment = new Payment(order, request.method(), order.getTotalAmount());
        payment.setTransactionId(generateTransactionId());
        return PaymentResponse.from(paymentRepository.save(payment));
    }

    @Transactional(rollbackFor = Exception.class)
    public void handlePaymentCallback(String transactionId, String status) {
        Payment payment = paymentRepository.findByTransactionId(transactionId)
            .orElseThrow(() -> new ResourceNotFoundException("Payment", 0L));

        if ("COMPLETED".equals(status) && "PENDING".equals(payment.getStatus())) {
            payment.setStatus("COMPLETED");
            payment.setPaidAt(Instant.now());
            payment.getOrder().setStatus("PAID");
        } else if ("FAILED".equals(status)) {
            payment.setStatus("FAILED");
        }
    }

    private String generateTransactionId() {
        return "TXN-" + UUID.randomUUID().toString().replace("-", "").substring(0, 16).toUpperCase();
    }
}

الناتج:

TEXT
// التنفيذ ناجح

7. الخصائص العامة

(1) ▶ مثال: تهيئة توثيق واجهة برمجة التطبيقات SpringDoc

JAVA
@Configuration
public class OpenApiConfig {

    @Bean
    public OpenAPI orderFlowOpenAPI() {
        return new OpenAPI()
            .info(new Info()
                .title("OrderFlow API")
                .version("1.0.0")
                .description("واجهة برمجة تطبيقات نظام إدارة طلبات التجارة الإلكترونية"))
            .addSecurityItem(new SecurityRequirement().addList("Bearer Auth"))
            .schemaRequirement("Bearer Auth",
                new SecurityScheme()
                    .type(SecurityScheme.Type.HTTP)
                    .scheme("bearer")
                    .bearerFormat("JWT"));
    }
}

الناتج:

TEXT
// التنفيذ ناجح
الخصائص العامة التنفيذ النطاق
معالجة الاستثناءات @RestControllerAdvice تنسيق خطأ معياري عالمي
سجلات الطلبات HandlerInterceptor سجلات جميع الطلبات الواردة
التحقق من المعاملات Bean Validation جميع DTOs للطلبات
توثيق واجهة برمجة التطبيقات SpringDoc OpenAPI توليد Swagger UI تلقائيًا
المصادقة الأمنية Spring Security + JWT جميع الواجهات المحمية

8. مثال شامل: هيكل المشروع الكامل لـ OrderFlow

TEXT
orderflow-service/
├── src/main/java/com/orderflow/
│   ├── OrderFlowApplication.java
│   ├── config/
│   │   ├── SecurityConfig.java          # JWT + مصادقة قائمة على الأدوار
│   │   ├── CacheConfig.java             # Caffeine + Redis
│   │   ├── AsyncConfig.java             # تجمع خيوط + جدولة
│   │   └── OpenApiConfig.java           # توثيق واجهة برمجة التطبيقات
│   ├── controller/
│   │   ├── AuthController.java          # تسجيل الدخول / التسجيل
│   │   ├── ProductController.java       # CRUD المنتجات
│   │   ├── OrderController.java         # إدارة الطلبات
│   │   └── PaymentController.java       # معالجة المدفوعات
│   ├── service/
│   │   ├── AuthService.java
│   │   ├── ProductService.java
│   │   ├── OrderService.java
│   │   ├── PaymentService.java
│   │   └── NotificationService.java     # إشعارات @Async
│   ├── repository/
│   │   ├── UserRepository.java
│   │   ├── ProductRepository.java
│   │   ├── OrderRepository.java
│   │   └── PaymentRepository.java
│   ├── model/
│   │   ├── User.java
│   │   ├── Product.java
│   │   ├── Order.java
│   │   ├── OrderItem.java
│   │   └── Payment.java
│   ├── dto/
│   │   ├── request/                     # CreateOrderRequest، إلخ.
│   │   └── response/                    # OrderResponse، إلخ.
│   ├── exception/
│   │   ├── BusinessException.java
│   │   ├── ResourceNotFoundException.java
│   │   ├── GlobalExceptionHandler.java
│   │   └── ErrorResponse.java
│   ├── security/
│   │   ├── JwtConfig.java               # زوج مفاتيح RSA
│   │   ├── CustomUserDetailsService.java
│   │   └── OrderSecurity.java           # مساعد @PreAuthorize
│   └── metrics/
│       └── OrderMetrics.java            # مقاييس Micrometer مخصصة
├── src/main/resources/
│   ├── application.yml
│   ├── application-dev.yml
│   ├── application-prod.yml
│   └── db/migration/                    # نصوص Flyway
│       ├── V1__init_schema.sql
│       └── V2__add_indexes.sql
├── Dockerfile
├── docker-compose.yml
└── pom.xml

❓ أسئلة شائعة

س هل أكتب Entity أولاً أم Controller؟
ج نوصي بمنهج تصاعدي: Entity ← Repository ← Service ← Controller. أولاً، حدد نموذج البيانات؛ ثم نفذ الوصول إلى البيانات؛ بعد ذلك، اكتب منطق الأعمال؛ وأخيرًا، نفذ طبقة واجهة برمجة التطبيقات. بهذه الطريقة، يمكن اختبار كل طبقة بشكل مستقل.
س كيف نضمن ترابطية ردود اتصال الدفع؟
ج 1) Redis SETNX يضمن بدء الدفع مرة واحدة فقط لكل طلب؛ 2) قيد فريد في قاعدة البيانات (transaction_id)؛ 3) آلة حالة الدفع (PENDING ← COMPLETED لا رجعة فيها)؛ 4) تسجيل جميع أحداث رد الاتصال لأغراض التسوية.
س كيف يضمن التخزين المؤقت ثنائي المستوى الاتساق؟
ج أثناء التحديث: 1) حدّث قاعدة البيانات أولاً؛ 2) احذف ذاكرة التخزين المؤقت Redis؛ 3) أبلغ جميع المثيلات عبر Redis Pub/Sub لمسح ذاكرات التخزين المؤقت Caffeine المحلية. للقراءة: تحقق من Caffeine أولاً ← ثم Redis ← أخيرًا قاعدة البيانات.
س كيف يتم إبقاء توثيق واجهة برمجة التطبيقات والكود متزامنين؟
ج يمسح SpringDoc تلقائيًا تعليقات Controller التوضيحية لتوليد توثيق OpenAPI؛ عندما يتغير الكود، يتحدث التوثيق تلقائيًا. ومع ذلك، تحتاج إلى إضافة تعليق @Schema التوضيحي إلى DTOs لتوفير شروحات إضافية.
س ما هي اصطلاحات تسمية نصوص ترحيل Flyway؟
ج V{version}__{description}.sql، مثل V1__init_schema.sql وV2__add_indexes.sql. يجب أن تكون أرقام الإصدارات متسلسلة، ويجب عدم تعديل النصوص التي تم تنفيذها بالفعل.
س كيف تنظم التطوير التعاوني بين عدة أشخاص؟
ج 1) أنشئ فروع Git حسب الوحدة (feature/user، feature/order)؛ 2) ادمج التغييرات في فرع develop يوميًا؛ 3) ادمج بعد مراجعة الكود؛ 4) شغّل الاختبارات تلقائيًا عبر CI. المفتاح هو ضمان أن كل وحدة مكتفية ذاتيًا وقابلة للاختبار.

📖 ملخص


📝 تمارين

  1. تمرين أساسي (الصعوبة: ⭐): نفذ كودًا كاملاً لتسجيل/تسجيل دخول المستخدمين وعمليات CRUD للمنتجات، مع التأكد من تفعيل مصادقة JWT والتخزين المؤقت.

  2. تمرين متقدم (الصعوبة: ⭐⭐): نفذ وحدة الطلبات (تقديم طلب، إلغاء طلب، والإلغاء التلقائي بسبب انتهاء المهلة) ووحدة المدفوعات (بدء الدفع وردود الاتصال الترابطية)، واستخدم Postman لإجراء اختبار شامل للعملية التجارية.

  3. تحدٍ (الصعوبة: ⭐⭐⭐): أضف توثيق SpringDoc لواجهة برمجة التطبيقات إلى OrderFlow وهيئ Swagger UI للاختبار التفاعلي؛ أضف نصوص ترحيل Flyway لقاعدة البيانات؛ اكتب اختبارات تكامل للعملية الأساسية باستخدام TestContainers.

Web-Tutorial.com

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

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

100%