تمرين عملي شامل: تطوير مشروع OrderFlow
تطوير المشروع هو عملية تحويل التصميم إلى كود—التقدم بطريقة معيارية، مع كل خطوة قابلة للتحقق والتكامل المستمر لمنع تراكم العمل.
1. ما ستتعلمه
- وحدة المستخدمين: التسجيل / تسجيل الدخول، إصدار JWT / التحكم في الوصول القائم على الأدوار
- وحدة المنتجات: CRUD / تخزين Redis المؤقت / تخزين Caffeine المؤقت من المستوى الثاني
- وحدة الطلبات: تقديم الطلبات / الإلغاء / الإلغاء التلقائي بسبب انتهاء المهلة / معاملات خصم المخزون
- وحدة المدفوعات: مدفوعات محاكاة / معالجة رد الاتصال / ضمان الترابطية
- الميزات العامة: معالجة الاستثناءات / تسجيل الطلبات / التحقق من المعاملات / توثيق واجهة برمجة التطبيقات (SpringDoc)
2. قصة حقيقية لمطور Full-Stack
(1) نقطة الألم: الفجوة بين التصميم والكود
أكملت Alice تصميم المنظومة لـ OrderFlow، لكنها تشعر بالإرهاق من حجم الكود الهائل—5 جداول، 20 واجهة برمجة تطبيقات، و4 وحدات. هل تبدأ بكتابة كود قاعدة البيانات أم كود واجهة برمجة التطبيقات؟ كيف يجب أن تعتمد الوحدات على بعضها البعض؟ كيف يمكنها التأكد من أن كل خطوة تعمل وتجتاز التحقق؟
(2) حلول التطوير المعياري
تقدم خطوة بخطوة على أساس الوحدات، مع كل وحدة قابلة للتحقق بشكل مستقل:
graph TD
A["وحدة المستخدمين<br/>المصادقة + JWT"] --> B["وحدة المنتجات<br/>CRUD + تخزين مؤقت"]
B --> C["وحدة الطلبات<br/>معاملات + جدولة"]
C --> D["وحدة المدفوعات<br/>رد الاتصال + ترابطية"]
D --> E["المقاطع العرضية<br/>استثناءات + تسجيل + توثيق"]
(3) الإيرادات
طورت Alice المشروع وحدة تلو الأخرى، مشغّلة الاختبارات للتحقق من كل وحدة عند إكمالها. أنهت الكود بالكامل في غضون أسبوعين، متجنبة مشاكل "الانفجار الكبير" المرتبطة بالتكامل لمرة واحدة.
3. وحدة المستخدمين
(1) ▶ مثال: كيان المستخدم والمستودع
@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);
}
الناتج:
// التنفيذ ناجح
(2) ▶ مثال: التسجيل وتسجيل الدخول
@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);
}
}
الناتج:
// التنفيذ ناجح
4. وحدة المنتجات
(1) ▶ مثال: ProductService مع التخزين المؤقت
@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);
}
}
الناتج:
// التنفيذ ناجح
5. وحدة الطلبات
(1) ▶ مثال: معاملة طلب OrderService
@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());
}
}
}
الناتج:
// التنفيذ ناجح
6. وحدة المدفوعات
(1) ▶ مثال: PaymentService مع الترابطية
@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();
}
}
الناتج:
// التنفيذ ناجح
7. الخصائص العامة
(1) ▶ مثال: تهيئة توثيق واجهة برمجة التطبيقات SpringDoc
@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"));
}
}
الناتج:
// التنفيذ ناجح
| الخصائص العامة | التنفيذ | النطاق |
|---|---|---|
| معالجة الاستثناءات | @RestControllerAdvice | تنسيق خطأ معياري عالمي |
| سجلات الطلبات | HandlerInterceptor | سجلات جميع الطلبات الواردة |
| التحقق من المعاملات | Bean Validation | جميع DTOs للطلبات |
| توثيق واجهة برمجة التطبيقات | SpringDoc OpenAPI | توليد Swagger UI تلقائيًا |
| المصادقة الأمنية | Spring Security + JWT | جميع الواجهات المحمية |
8. مثال شامل: هيكل المشروع الكامل لـ OrderFlow
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
❓ أسئلة شائعة
@Schema التوضيحي إلى DTOs لتوفير شروحات إضافية.V{version}__{description}.sql، مثل V1__init_schema.sql وV2__add_indexes.sql. يجب أن تكون أرقام الإصدارات متسلسلة، ويجب عدم تعديل النصوص التي تم تنفيذها بالفعل.📖 ملخص
- وحدة المستخدمين: تجزئة كلمات المرور BCrypt، إصدار JWT، التحكم في الوصول القائم على الأدوار
- وحدة المنتجات: @Cacheable للتخزين المؤقت ثنائي المستوى، @CachePut للتحديثات المتزامنة، @CacheEvict للإخلاء
- وحدة الطلبات: @Transactional لخصم المخزون عند تقديم الطلب، @Scheduled للإلغاء التلقائي عند انتهاء المهلة، وإشعارات Pub/Sub
- وحدة المدفوعات: ترابطية Redis SETNX، ضمانات آلة الحالة، معالجة رد الاتصال
- الميزات العامة: @RestControllerAdvice لمعالجة استثناءات موحدة، توثيق SpringDoc لواجهة برمجة التطبيقات، Bean Validation
📝 تمارين
-
تمرين أساسي (الصعوبة: ⭐): نفذ كودًا كاملاً لتسجيل/تسجيل دخول المستخدمين وعمليات CRUD للمنتجات، مع التأكد من تفعيل مصادقة JWT والتخزين المؤقت.
-
تمرين متقدم (الصعوبة: ⭐⭐): نفذ وحدة الطلبات (تقديم طلب، إلغاء طلب، والإلغاء التلقائي بسبب انتهاء المهلة) ووحدة المدفوعات (بدء الدفع وردود الاتصال الترابطية)، واستخدم Postman لإجراء اختبار شامل للعملية التجارية.
-
تحدٍ (الصعوبة: ⭐⭐⭐): أضف توثيق SpringDoc لواجهة برمجة التطبيقات إلى OrderFlow وهيئ Swagger UI للاختبار التفاعلي؛ أضف نصوص ترحيل Flyway لقاعدة البيانات؛ اكتب اختبارات تكامل للعملية الأساسية باستخدام TestContainers.



