Spring Boot: 综合实战:OrderFlow 项目开发

最后更新:2026-08-26

项目开发是把设计变成代码的过程——模块化推进,每步可验证,持续集成不堆积。

1. 你将学到


2. 一个全栈开发者的真实故事

(1) 痛点:设计到代码的跨越

Alice 完成了 OrderFlow 的系统设计,面对 5 张表、20 个 API、4 个模块的代码量感到无从下手。直接从数据库开始写还是从 API 开始?模块间怎么依赖?如何保证每步都能运行验证?

(2) 模块化开发的解法

按模块逐步推进,每个模块可独立验证:

100%
graph TD
    A["User Module<br/>Auth + JWT"] --> B["Product Module<br/>CRUD + Cache"]
    B --> C["Order Module<br/>Transaction + Schedule"]
    C --> D["Payment Module<br/>Callback + Idempotent"]
    D --> E["Cross-cutting<br/>Exception + Logging + Doc"]

(3) 收益

Alice 按模块逐步开发,每完成一个模块就运行测试验证,2 周内完成全部代码,没有一次性集成的大爆炸问题。


3. 用户模块

▶ 示例: User Entity 与 Repository

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 📖 仅展示
// 执行成功

▶ 示例: 注册与登录

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", "Username already taken");
        }
        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. 商品模块

▶ 示例: ProductService with Caching

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. 订单模块

▶ 示例: 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("Auto-cancelled {} expired orders", expired.size());
        }
    }
}

输出:

TEXT 📖 仅展示
// 执行成功

6. 支付模块

▶ 示例: PaymentService with Idempotency

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");
        }

        // Idempotency check: same transaction_id should not create duplicate payment
        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",
                "Payment already initiated for order " + 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. 全局特性

▶ 示例: SpringDoc API 文档配置

JAVA
@Configuration
public class OpenApiConfig {

    @Bean
    public OpenAPI orderFlowOpenAPI() {
        return new OpenAPI()
            .info(new Info()
                .title("OrderFlow API")
                .version("1.0.0")
                .description("E-commerce order management system API"))
            .addSecurityItem(new SecurityRequirement().addList("Bearer Auth"))
            .schemaRequirement("Bearer Auth",
                new SecurityScheme()
                    .type(SecurityScheme.Type.HTTP)
                    .scheme("bearer")
                    .bearerFormat("JWT"));
    }
}

输出:

TEXT 📖 仅展示
// 执行成功
全局特性 实现方式 覆盖范围
异常处理 @RestControllerAdvice 全局统一错误格式
请求日志 HandlerInterceptor 所有请求入站日志
参数校验 Bean Validation 所有请求 DTO
API 文档 SpringDoc OpenAPI 自动生成 Swagger UI
安全认证 Spring Security + JWT 所有受保护接口

8. 综合示例:OrderFlow 完整项目结构

TEXT 📖 仅展示
orderflow-service/
├── src/main/java/com/orderflow/
│   ├── OrderFlowApplication.java
│   ├── config/
│   │   ├── SecurityConfig.java          # JWT + Role-based auth
│   │   ├── CacheConfig.java             # Caffeine + Redis
│   │   ├── AsyncConfig.java             # Thread pool + Scheduling
│   │   └── OpenApiConfig.java           # API documentation
│   ├── controller/
│   │   ├── AuthController.java          # Login / Register
│   │   ├── ProductController.java       # Product CRUD
│   │   ├── OrderController.java         # Order management
│   │   └── PaymentController.java       # Payment processing
│   ├── service/
│   │   ├── AuthService.java
│   │   ├── ProductService.java
│   │   ├── OrderService.java
│   │   ├── PaymentService.java
│   │   └── NotificationService.java     # @Async notifications
│   ├── repository/
│   │   ├── UserRepository.java
│   │   ├── ProductRepository.java
│   │   ├── OrderRepository.java
│   │   └── PaymentRepository.java
│   ├── model/
│   │   ├── User.java
│   │   ├── Product.java
│   │   ├── Order.java
│   │   ├── OrderItem.java
│   │   └── Payment.java
│   ├── dto/
│   │   ├── request/                     # CreateOrderRequest, etc.
│   │   └── response/                    # OrderResponse, etc.
│   ├── exception/
│   │   ├── BusinessException.java
│   │   ├── ResourceNotFoundException.java
│   │   ├── GlobalExceptionHandler.java
│   │   └── ErrorResponse.java
│   ├── security/
│   │   ├── JwtConfig.java               # RSA key pair
│   │   ├── CustomUserDetailsService.java
│   │   └── OrderSecurity.java           # @PreAuthorize helper
│   └── metrics/
│       └── OrderMetrics.java            # Custom Micrometer metrics
├── src/main/resources/
│   ├── application.yml
│   ├── application-dev.yml
│   ├── application-prod.yml
│   └── db/migration/                    # Flyway scripts
│       ├── V1__init_schema.sql
│       └── V2__add_indexes.sql
├── Dockerfile
├── docker-compose.yml
└── pom.xml

❓ 常见问题

Q 先写 Entity 还是先写 Controller?
A 推荐从内到外:Entity → Repository → Service → Controller。先定义数据模型,再实现数据访问,再写业务逻辑,最后写 API 层。这样每层都可以独立测试。
Q 如何保证支付回调的幂等性?
A 1)Redis SETNX 保证同一订单只发起一次支付;2)数据库唯一约束(transaction_id);3)支付状态机(PENDING → COMPLETED 不可逆);4)记录所有回调日志用于对账。
Q 两级缓存如何保证一致性?
A 更新时:1)先更新数据库;2)删除 Redis 缓存;3)通过 Redis Pub/Sub 通知所有实例清除 Caffeine 本地缓存。读取时:先查 Caffeine → 再查 Redis → 最后查数据库。
Q API 文档和代码如何保持同步?
A SpringDoc 自动扫描 Controller 注解生成 OpenAPI 文档,代码改了文档自动更新。但需要在 DTO 上加 @Schema 注解补充说明。
Q Flyway 迁移脚本的命名规则是什么?
A V{version}__{description}.sql,如 V1__init_schema.sqlV2__add_indexes.sql。版本号必须递增,已执行的脚本不可修改。
Q 如何组织多人协作开发?
A 1)按模块分 Git 分支(feature/user、feature/order);2)每日集成到 develop 分支;3)Code Review 合并;4)CI 自动运行测试。每个模块独立可测试是关键。

📖 小节


📝 作业

  1. 基础题(难度⭐):实现用户注册/登录和商品 CRUD 的完整代码,确保 JWT 认证和缓存生效。

  2. 进阶题(难度⭐⭐):实现订单模块(下单、取消、超时自动取消)和支付模块(发起支付、幂等回调),使用 Postman 完成端到端业务流程测试。

  3. 挑战题(难度⭐⭐⭐):为 OrderFlow 添加 SpringDoc API 文档,配置 Swagger UI 可交互测试;添加 Flyway 数据库迁移脚本;使用 TestContainers 编写核心流程的集成测试。

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏