Spring Boot: Phase 2 综合练习:完善 OrderFlow 核心业务

最后更新:2026-08-26

整合是检验理解的试金石——把 JPA、事务、验证、异常处理、安全融为一个可运行的完整系统。

1. 你将学到


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

(1) 痛点:模块各自为政

Alice 完成了 JPA、Service、Validation、Exception Handling、Security 五个模块的学习,但它们都是独立存在的。真正下单时,需要把"验证输入 → 检查库存 → 扣减库存 → 创建订单 → 事务保障 → 权限控制 → 异常处理"全部串联起来,她不确定各模块之间如何协作。

(2) 综合练习的解法

本课将所有模块整合为一个完整的下单流程,模拟真实业务场景。

(3) 收益

Alice 完成整合后,OrderFlow 具备了完整的业务闭环能力——从用户登录到下单、查询、取消,每个环节都有验证、事务、权限、异常处理的保护。


3. 数据模型整合

(1) 完整 ER 关系

100%
erDiagram
    PRODUCT ||--o{ ORDER_ITEM : "included in"
    ORDER ||--o{ ORDER_ITEM : "contains"
    USER ||--o{ ORDER : "places"

    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
    }

▶ 示例: User Entity

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 Client
    participant Ctrl as OrderController
    participant Val as Validation
    participant Svc as OrderService
    participant Tx as Transaction
    participant Repo as Repository

    C->>Ctrl: POST /api/v1/orders
    Ctrl->>Val: @Valid CreateOrderRequest
    alt Validation fails
        Val-->>C: 400 VALIDATION_ERROR
    end
    Ctrl->>Svc: createOrder(request)
    Svc->>Tx: Begin Transaction
    Svc->>Repo: findProductById
    alt Product not found
        Svc-->>C: 404 RESOURCE_NOT_FOUND
    end
    Svc->>Repo: deductStock (check quantity)
    alt Insufficient stock
        Tx->>Tx: Rollback
        Svc-->>C: 409 INSUFFICIENT_STOCK
    end
    Svc->>Repo: save Order + OrderItems
    Tx->>Tx: Commit
    Svc-->>Ctrl: Order created
    Ctrl-->>C: 201 Created

▶ 示例: 完整 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. Service 层事务整合

▶ 示例: 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. DTO 与验证整合

▶ 示例: 请求/响应 DTO

JAVA
// Request DTO with validation
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
) {}

// Response 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. 安全整合

▶ 示例: 自定义 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();
    }
}

// OrderSecurity helper for SpEL expressions
@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. Seed product data
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. Create order as customer
curl -u bob:pass123 -X POST http://localhost:8080/api/v1/orders \
  -H "Content-Type: application/json" \
  -d '{"productId":1,"quantity":3}'

# 3. View own order (customer bob)
curl -u bob:pass123 http://localhost:8080/api/v1/orders/1

# 4. Try invalid quantity -> 400
curl -u bob:pass123 -X POST http://localhost:8080/api/v1/orders \
  -H "Content-Type: application/json" \
  -d '{"productId":1,"quantity":0}'

# 5. Try ordering more than stock -> 409
curl -u bob:pass123 -X POST http://localhost:8080/api/v1/orders \
  -H "Content-Type: application/json" \
  -d '{"productId":1,"quantity":999}'

# 6. Cancel order as admin
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 生效

❓ 常见问题

Q 各模块之间如何正确分层调用?
A Controller → Service → Repository。Controller 负责参数验证和权限检查,Service 负责业务逻辑和事务,Repository 负责数据访问。不要跨层调用。
Q 异常处理和事务如何协作?
A 业务异常在 Service 中抛出,由 GlobalExceptionHandler 统一处理。事务检测到异常自动回滚。注意不要在 Service 中 catch 异常后不抛出,否则事务不会回滚。
Q @PreAuthorize 中的 SpEL 表达式怎么调试?
A 设置 logging.level.org.springframework.security=DEBUG,可以看到访问决策日志。也可以在 SpEL 中引用的 Bean 方法中加日志。
Q 如何测试安全配置?
A 使用 @SpringBootTest + @WithMockUser 注解模拟认证用户。@WithMockUser(roles="ADMIN") 模拟 ADMIN 角色。
Q 端到端测试应该覆盖哪些场景?
A 至少覆盖:正常流程、验证失败、业务异常(库存不足)、权限拒绝(非本人订单)、未认证访问。每个场景验证状态码和响应格式。
Q Phase 2 完成后项目可以上线吗?
A 基本功能完整但还不够。缺少:JWT 认证(Phase 3)、缓存(Phase 3)、监控(Phase 4)、容器化(Phase 4)、CI/CD(Phase 5)。建议完成所有 Phase 后再上线。

📖 小节


📝 作业

  1. 基础题(难度⭐):完成本课所有代码整合,确保下单、查询、取消的端到端流程全部通过 curl 测试。

  2. 进阶题(难度⭐⭐):添加商品管理模块(ProductController + ProductService),实现 ADMIN 角色可以创建/更新/删除商品,普通用户只能查询,添加完整的验证和异常处理。

  3. 挑战题(难度⭐⭐⭐):使用 @SpringBootTest + @WithMockUser 编写集成测试,覆盖下单成功、库存不足、权限拒绝等场景,测试覆盖率达到核心流程 80% 以上。

Web-Tutorial.com

Web-Tutorial 技术团队

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

100%

🙏 帮我们做得更好

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

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