تمرين المرحلة 2 الشامل: إكمال وظائف الأعمال الأساسية لـ OrderFlow
الدمج هو الاختبار الحقيقي للفهم—دمج JPA، المعاملات، التحقق، معالجة الاستثناءات، والأمان في نظام واحد يعمل بالكامل.
1. ما ستتعلمه
- تنفيذ وصلات الجداول المتعددة والعمليات المتسلسلة بين Order، OrderItem، وProduct
- تحسين عملية تقديم الطلبات: التحقق من المخزون → خصم المخزون → إنشاء الطلب → ضمان المعاملة
- معالجة الاستثناءات الموحدة تغطي جميع سيناريوهات الأعمال
- التحكم في صلاحيات API حسب الدور (CUSTOMER / ADMIN)
- استخدام وحدة تحكم H2 وPostman لإجراء اختبار شامل لعمليات الأعمال
2. قصة حقيقية لمطوّر متكامل
(1) نقطة الألم: الوحدات تعمل بمعزل عن بعضها
أكملت Alice دراستها للوحدات الخمس—JPA، الخدمة، التحقق، معالجة الاستثناءات، والأمان—لكنها جميعها موجودة بمعزل. عند تقديم طلب فعلياً، تحتاج إلى ربط العملية بالكامل: "التحقق من الإدخال → فحص المخزون → خصم المخزون → إنشاء الطلب → ضمان سلامة المعاملة → فرض التحكم في الوصول → معالجة الاستثناءات." وهي غير متأكدة كيف تعمل الوحدات المختلفة معاً.
(2) حلول التمارين الشاملة
يربط هذا الدرس جميع الوحدات في عملية تقديم طلب كاملة، محاكياً سيناريو أعمال واقعي.
(3) العوائد
بعد إكمال Alice للدمج، أصبح OrderFlow يمتلك قدرة أعمال حلقة مغلقة بالكامل—من تسجيل دخول المستخدم إلى تقديم الطلبات، والاستعلام، والإلغاء، كل خطوة محمية بالتحقق، والمعاملات، والصلاحيات، ومعالجة الاستثناءات.
3. دمج نموذج البيانات
(1) نموذج ER الكامل
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) ▶ مثال: كيان المستخدم
@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
}
الناتج:
// التنفيذ ناجح
4. دمج عملية تقديم الطلبات
(1) الجدول الزمني الكامل للطلب
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 كامل
@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();
}
}
الناتج:
// التنفيذ ناجح
5. دمج المعاملات في طبقة الخدمة
(1) ▶ مثال: التنفيذ الكامل لـ OrderService
@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);
}
}
الناتج:
// التنفيذ ناجح
6. دمج DTOs مع التحقق
(1) ▶ مثال: DTO للطلب/الاستجابة
// 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
) {}
الناتج:
// التنفيذ ناجح
7. دمج الأمان
(1) ▶ مثال: UserDetailsService مخصص
@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);
}
}
الناتج:
// التنفيذ ناجح
8. مثال شامل: اختبار عملية الأعمال من النهاية إلى النهاية
# 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 مُفعّل |
❓ أسئلة شائعة
logging.level.org.springframework.security=DEBUG لعرض سجلات قرار الوصول. يمكنك أيضاً إضافة سجلات إلى طرق Bean المُشار إليها في تعبير SpEL.@WithMockUser(roles="ADMIN") يحاكي دور ADMIN.📖 ملخص
- دمج JPA، الخدمات، التحقق، الاستثناءات، والأمان لبناء سير عمل أعمال كامل
- عملية تقديم الطلبات: التحقق من الإدخال → فحص المخزون → خصم المخزون → إنشاء الطلب → تأكيد المعاملة
- مسؤوليات واضحة لكل وحدة: المتحكم (التحقق/التفويض) → الخدمة (منطق الأعمال/المعاملات) → المستودع (البيانات)
- تخصيص UserDetailsService لاستبدال المستخدمين في الذاكرة؛ Bean مساعد OrderSecurity لفحوصات الصلاحيات على مستوى الطريقة
- الاختبار من النهاية إلى النهاية يغطي التدفق الطبيعي + التحقق من الفشل + استثناءات الأعمال + رفض الصلاحيات
📝 تمارين
-
تمرين أساسي (الصعوبة: ⭐): أكمل جميع دمج الكود لهذا الدرس وتأكد من نجاح عمليات تقديم الطلبات والاستعلام والإلغاء من النهاية إلى النهاية في اختبارات curl.
-
تمرين متقدم (الصعوبة ⭐⭐): أضف وحدة إدارة المنتجات (ProductController + ProductService) التي تسمح للمستخدمين بدور ADMIN بإنشاء المنتجات وتحديثها وحذفها، بينما يمكن للمستخدمين العاديين الاستعلام فقط. أضف تحققات شاملة ومعالجة استثناءات.
-
تحدٍ (الصعوبة: ⭐⭐⭐): استخدم
@SpringBootTest+@WithMockUserلكتابة اختبارات تكامل تغطي سيناريوهات مثل تقديم طلب ناجح، مخزون غير كافٍ، ورفض صلاحيات، مع تحقيق معدل تغطية اختبار لا يقل عن 80% للتدفق الأساسي.



