404 Not Found

404 Not Found


nginx

تمرين المرحلة 2 الشامل: إكمال وظائف الأعمال الأساسية لـ OrderFlow

الدمج هو الاختبار الحقيقي للفهم—دمج JPA، المعاملات، التحقق، معالجة الاستثناءات، والأمان في نظام واحد يعمل بالكامل.

1. ما ستتعلمه


2. قصة حقيقية لمطوّر متكامل

(1) نقطة الألم: الوحدات تعمل بمعزل عن بعضها

أكملت Alice دراستها للوحدات الخمس—JPA، الخدمة، التحقق، معالجة الاستثناءات، والأمان—لكنها جميعها موجودة بمعزل. عند تقديم طلب فعلياً، تحتاج إلى ربط العملية بالكامل: "التحقق من الإدخال → فحص المخزون → خصم المخزون → إنشاء الطلب → ضمان سلامة المعاملة → فرض التحكم في الوصول → معالجة الاستثناءات." وهي غير متأكدة كيف تعمل الوحدات المختلفة معاً.

(2) حلول التمارين الشاملة

يربط هذا الدرس جميع الوحدات في عملية تقديم طلب كاملة، محاكياً سيناريو أعمال واقعي.

(3) العوائد

بعد إكمال Alice للدمج، أصبح OrderFlow يمتلك قدرة أعمال حلقة مغلقة بالكامل—من تسجيل دخول المستخدم إلى تقديم الطلبات، والاستعلام، والإلغاء، كل خطوة محمية بالتحقق، والمعاملات، والصلاحيات، ومعالجة الاستثناءات.


3. دمج نموذج البيانات

(1) نموذج ER الكامل

100%
erDiagram
    PRODUCT ||--o{ ORDER_ITEM : "مشمول في"
    ORDER ||--o{ ORDER_ITEM : "يحتوي على"
    USER ||--o{ ORDER : "يُقدّم"

    PRODUCT {
        bigint id PK
        varchar name
        decimal price
        int stock
        varchar sku
    }
    ORDER {
        bigint id PK
        bigint user_id FK
        varchar status
        decimal total_amount
        timestamp created_at
    }
    ORDER_ITEM {
        bigint id PK
        bigint order_id FK
        bigint product_id FK
        int quantity
        decimal unit_price
    }
    USER {
        bigint id PK
        varchar username
        varchar password
        varchar email
        varchar role
    }

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

JAVA
@Entity
@Table(name = "users")
public class User {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

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

    @Column(nullable = false)
    private String password;

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

    @Column(nullable = false)
    private String role;

    protected User() {}

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

الناتج:

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

4. دمج عملية تقديم الطلبات

(1) الجدول الزمني الكامل للطلب

100%
sequenceDiagram
    participant C as العميل
    participant Ctrl as OrderController
    participant Val as التحقق
    participant Svc as OrderService
    participant Tx as المعاملة
    participant Repo as المستودع

    C->>Ctrl: POST /api/v1/orders
    Ctrl->>Val: @Valid CreateOrderRequest
    alt فشل التحقق
        Val-->>C: 400 VALIDATION_ERROR
    end
    Ctrl->>Svc: createOrder(request)
    Svc->>Tx: بدء المعاملة
    Svc->>Repo: findProductById
    alt المنتج غير موجود
        Svc-->>C: 404 RESOURCE_NOT_FOUND
    end
    Svc->>Repo: deductStock (فحص الكمية)
    alt مخزون غير كافٍ
        Tx->>Tx: تراجع
        Svc-->>C: 409 INSUFFICIENT_STOCK
    end
    Svc->>Repo: حفظ Order + OrderItems
    Tx->>Tx: تأكيد
    Svc-->>Ctrl: تم إنشاء الطلب
    Ctrl-->>C: 201 Created

(1) ▶ مثال: OrderController كامل

JAVA
@RestController
@RequestMapping("/api/v1/orders")
public class OrderController {

    private final OrderService orderService;

    public OrderController(OrderService orderService) {
        this.orderService = orderService;
    }

    @PostMapping
    @PreAuthorize("hasAnyRole('CUSTOMER', 'ADMIN')")
    public ResponseEntity<OrderResponse> createOrder(
            @Valid @RequestBody CreateOrderRequest request,
            @AuthenticationPrincipal UserDetails userDetails) {
        OrderResponse order = orderService.createOrder(
            request, userDetails.getUsername());
        return ResponseEntity.status(HttpStatus.CREATED).body(order);
    }

    @GetMapping("/{id}")
    @PreAuthorize("hasRole('ADMIN') or @orderSecurity.isOwner(#id, authentication)")
    public ResponseEntity<OrderResponse> getOrder(@PathVariable Long id) {
        return ResponseEntity.ok(orderService.getOrder(id));
    }

    @DeleteMapping("/{id}")
    @PreAuthorize("hasRole('ADMIN')")
    public ResponseEntity<Void> cancelOrder(@PathVariable Long id) {
        orderService.cancelOrder(id);
        return ResponseEntity.noContent().build();
    }
}

الناتج:

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

5. دمج المعاملات في طبقة الخدمة

(1) ▶ مثال: التنفيذ الكامل لـ OrderService

JAVA
@Service
public class OrderService {

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

    public OrderService(OrderRepository orderRepository,
                        ProductRepository productRepository,
                        UserRepository userRepository) {
        this.orderRepository = orderRepository;
        this.productRepository = productRepository;
        this.userRepository = userRepository;
    }

    @Transactional(rollbackFor = Exception.class)
    public OrderResponse createOrder(CreateOrderRequest request, String username) {
        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);

        return OrderResponse.from(saved);
    }

    @Transactional(rollbackFor = Exception.class)
    public void cancelOrder(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");
    }

    @Transactional(readOnly = true)
    public OrderResponse getOrder(Long orderId) {
        Order order = orderRepository.findByIdWithItems(orderId)
            .orElseThrow(() -> new ResourceNotFoundException("Order", orderId));
        return OrderResponse.from(order);
    }
}

الناتج:

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

6. دمج DTOs مع التحقق

(1) ▶ مثال: DTO للطلب/الاستجابة

JAVA
// DTO للطلب مع التحقق
public record CreateOrderRequest(
    @NotNull(message = "Product ID is required")
    Long productId,

    @Min(value = 1, message = "Quantity must be at least 1")
    @Max(value = 100, message = "Quantity cannot exceed 100")
    Integer quantity
) {}

// DTO للاستجابة
public record OrderResponse(
    Long id,
    String username,
    String status,
    BigDecimal totalAmount,
    List<OrderItemResponse> items,
    Instant createdAt
) {
    public static OrderResponse from(Order order) {
        List<OrderItemResponse> items = order.getItems().stream()
            .map(item -> new OrderItemResponse(
                item.getId(),
                item.getProduct().getName(),
                item.getQuantity(),
                item.getUnitPrice(),
                item.getUnitPrice().multiply(BigDecimal.valueOf(item.getQuantity()))
            ))
            .toList();
        return new OrderResponse(
            order.getId(),
            order.getUser().getUsername(),
            order.getStatus(),
            order.getTotalAmount(),
            items,
            order.getCreatedAt()
        );
    }
}

public record OrderItemResponse(
    Long id,
    String productName,
    Integer quantity,
    BigDecimal unitPrice,
    BigDecimal subtotal
) {}

الناتج:

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

7. دمج الأمان

(1) ▶ مثال: UserDetailsService مخصص

JAVA
@Service
public class CustomUserDetailsService implements UserDetailsService {

    private final UserRepository userRepository;

    public CustomUserDetailsService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Override
    public UserDetails loadUserByUsername(String username)
            throws UsernameNotFoundException {
        User user = userRepository.findByUsername(username)
            .orElseThrow(() -> new UsernameNotFoundException(
                "User not found: " + username));
        return User.builder()
            .username(user.getUsername())
            .password(user.getPassword())
            .roles(user.getRole())
            .build();
    }
}

// Bean مساعد OrderSecurity لتعبيرات SpEL
@Component("orderSecurity")
public class OrderSecurity {

    private final OrderRepository orderRepository;

    public OrderSecurity(OrderRepository orderRepository) {
        this.orderRepository = orderRepository;
    }

    public boolean isOwner(Long orderId, Authentication auth) {
        String username = auth.getName();
        return orderRepository.findById(orderId)
            .map(order -> order.getUser().getUsername().equals(username))
            .orElse(false);
    }
}

الناتج:

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

8. مثال شامل: اختبار عملية الأعمال من النهاية إلى النهاية

BASH
# 1. بذر بيانات المنتجات
curl -u alice:admin123 -X POST http://localhost:8080/api/v1/products \
  -H "Content-Type: application/json" \
  -d '{"name":"Laptop Pro","price":1299.99,"stock":50,"sku":"LAP-0001"}'

# 2. إنشاء طلب كعميل
curl -u bob:pass123 -X POST http://localhost:8080/api/v1/orders \
  -H "Content-Type: application/json" \
  -d '{"productId":1,"quantity":3}'

# 3. عرض طلبي الخاص (العميل bob)
curl -u bob:pass123 http://localhost:8080/api/v1/orders/1

# 4. تجربة كمية غير صالحة -> 400
curl -u bob:pass123 -X POST http://localhost:8080/api/v1/orders \
  -H "Content-Type: application/json" \
  -d '{"productId":1,"quantity":0}'

# 5. تجربة طلب يفوق المخزون -> 409
curl -u bob:pass123 -X POST http://localhost:8080/api/v1/orders \
  -H "Content-Type: application/json" \
  -d '{"productId":1,"quantity":999}'

# 6. إلغاء الطلب كمدير
curl -u alice:admin123 -X DELETE http://localhost:8080/api/v1/orders/1
سيناريو الاختبار النتيجة المتوقعة نقاط التحقق
إنشاء طلب 201 Created ضمان المعاملة: الطلبات والمخزون يتحدثان معاً
عرض طلباتي 200 OK التحكم في الوصول: المستخدم يمكنه رؤية طلباته فقط
كمية غير صالحة 400 VALIDATION_ERROR Bean Validation مُفعّل
مخزون غير كافٍ 409 INSUFFICIENT_STOCK استثناء أعمال → معالجة استثناءات عامة
إلغاء طلب 204 No Content المعاملة: استعادة المخزون
وصول غير مصادق 401 Unauthorized Spring Security مُفعّل

❓ أسئلة شائعة

س ما هو تسلسل الاستدعاء الطبقي الصحيح بين الوحدات؟
ج المتحكم → الخدمة → المستودع. المتحكم يتولى التحقق من المعاملات وفحوصات التفويض، الخدمة تتولى منطق الأعمال والمعاملات، والمستودع يتولى الوصول للبيانات. لا تقم باستدعاءات عبر الطبقات.
س كيف تعمل معالجة الاستثناءات والمعاملات معاً؟
ج استثناءات الأعمال تُرمى في الخدمة وتُعالج بشكل موحد بواسطة GlobalExceptionHandler. عندما تكتشف المعاملة استثناءً، تتراجع تلقائياً. احذر من التقاط استثناء في الخدمة ثم عدم إعادة رميه؛ وإلا لن تتراجع المعاملة.
س كيف أُصحح تعبيرات SpEL في @PreAuthorize؟
ج اضبط logging.level.org.springframework.security=DEBUG لعرض سجلات قرار الوصول. يمكنك أيضاً إضافة سجلات إلى طرق Bean المُشار إليها في تعبير SpEL.
س كيف أختبر تهيئة الأمان؟
ج استخدم التعليقات @SpringBootTest و@WithMockUser لمحاكاة مستخدم مصادق عليه. @WithMockUser(roles="ADMIN") يحاكي دور ADMIN.
س ما السيناريوهات التي يجب أن يغطيها الاختبار من النهاية إلى النهاية؟
ج كحد أدنى، يجب أن يغطي: التدفق الطبيعي، فشل التحقق، استثناءات الأعمال (مخزون غير كافٍ)، رفض الصلاحيات (طلبات مقدمة من شخص آخر)، والوصول غير المصادق. تحقق من رموز الحالة وتنسيقات الاستجابة لكل سيناريو.
س هل يمكن نشر المشروع بعد إكمال المرحلة 2؟
ج الوظائف الأساسية مكتملة، لكنها ليست كافية بعد. لا يزال ينقص: مصادقة JWT (المرحلة 3)، التخزين المؤقت (المرحلة 3)، المراقبة (المرحلة 4)، الحاويات (المرحلة 4)، وCI/CD (المرحلة 5). نوصي بإطلاق المشروع فقط بعد إكمال جميع المراحل.

📖 ملخص


📝 تمارين

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

  2. تمرين متقدم (الصعوبة ⭐⭐): أضف وحدة إدارة المنتجات (ProductController + ProductService) التي تسمح للمستخدمين بدور ADMIN بإنشاء المنتجات وتحديثها وحذفها، بينما يمكن للمستخدمين العاديين الاستعلام فقط. أضف تحققات شاملة ومعالجة استثناءات.

  3. تحدٍ (الصعوبة: ⭐⭐⭐): استخدم @SpringBootTest + @WithMockUser لكتابة اختبارات تكامل تغطي سيناريوهات مثل تقديم طلب ناجح، مخزون غير كافٍ، ورفض صلاحيات، مع تحقيق معدل تغطية اختبار لا يقل عن 80% للتدفق الأساسي.

Web-Tutorial.com

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

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

100%