Spring Boot: Phase 2 综合练习:完善 OrderFlow 核心业务
最后更新:2026-08-26
整合是检验理解的试金石——把 JPA、事务、验证、异常处理、安全融为一个可运行的完整系统。
1. 你将学到
- 实现 Order ↔ OrderItem ↔ Product 多表关联与级联操作
- 完善下单流程:验证库存 → 扣减库存 → 创建订单 → 事务保障
- 统一异常处理覆盖所有业务场景
- 按角色(CUSTOMER / ADMIN)控制接口权限
- 使用 H2 控制台与 Postman 完成端到端业务流程测试
2. 一个全栈开发者的真实故事
(1) 痛点:模块各自为政
Alice 完成了 JPA、Service、Validation、Exception Handling、Security 五个模块的学习,但它们都是独立存在的。真正下单时,需要把"验证输入 → 检查库存 → 扣减库存 → 创建订单 → 事务保障 → 权限控制 → 异常处理"全部串联起来,她不确定各模块之间如何协作。
(2) 综合练习的解法
本课将所有模块整合为一个完整的下单流程,模拟真实业务场景。
(3) 收益
Alice 完成整合后,OrderFlow 具备了完整的业务闭环能力——从用户登录到下单、查询、取消,每个环节都有验证、事务、权限、异常处理的保护。
3. 数据模型整合
(1) 完整 ER 关系
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) 完整下单时序
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 后再上线。
📖 小节
- 整合 JPA + Service + Validation + Exception + Security 构建完整业务闭环
- 下单流程:验证输入 → 检查库存 → 扣减库存 → 创建订单 → 事务提交
- 各模块职责清晰:Controller(验证/权限)→ Service(业务/事务)→ Repository(数据)
- 自定义 UserDetailsService 替换内存用户,OrderSecurity 辅助方法级权限判断
- 端到端测试覆盖正常流程 + 验证失败 + 业务异常 + 权限拒绝
📝 作业
-
基础题(难度⭐):完成本课所有代码整合,确保下单、查询、取消的端到端流程全部通过 curl 测试。
-
进阶题(难度⭐⭐):添加商品管理模块(ProductController + ProductService),实现 ADMIN 角色可以创建/更新/删除商品,普通用户只能查询,添加完整的验证和异常处理。
-
挑战题(难度⭐⭐⭐):使用
@SpringBootTest+@WithMockUser编写集成测试,覆盖下单成功、库存不足、权限拒绝等场景,测试覆盖率达到核心流程 80% 以上。