Spring Boot: 综合实战:OrderFlow 项目开发
最后更新:2026-08-26
项目开发是把设计变成代码的过程——模块化推进,每步可验证,持续集成不堆积。
1. 你将学到
- 用户模块:注册 / 登录 JWT 签发 / 角色权限控制
- 商品模块:CRUD / Redis 缓存 / Caffeine 二级缓存
- 订单模块:下单 / 取消 / 超时自动取消 / 库存扣减事务
- 支付模块:模拟支付 / 回调处理 / 幂等性保障
- 全局特性:异常处理 / 请求日志 / 参数校验 / API 文档(SpringDoc)
2. 一个全栈开发者的真实故事
(1) 痛点:设计到代码的跨越
Alice 完成了 OrderFlow 的系统设计,面对 5 张表、20 个 API、4 个模块的代码量感到无从下手。直接从数据库开始写还是从 API 开始?模块间怎么依赖?如何保证每步都能运行验证?
(2) 模块化开发的解法
按模块逐步推进,每个模块可独立验证:
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.sql、V2__add_indexes.sql。版本号必须递增,已执行的脚本不可修改。Q 如何组织多人协作开发?
A 1)按模块分 Git 分支(feature/user、feature/order);2)每日集成到 develop 分支;3)Code Review 合并;4)CI 自动运行测试。每个模块独立可测试是关键。
📖 小节
- 用户模块:BCrypt 密码编码、JWT 签发、角色权限控制
- 商品模块:@Cacheable 两级缓存、@CachePut 更新同步、@CacheEvict 删除清除
- 订单模块:@Transactional 下单扣库存、@Scheduled 超时自动取消、Pub/Sub 事件通知
- 支付模块:Redis SETNX 幂等性、状态机保证、回调处理
- 全局特性:@RestControllerAdvice 统一异常、SpringDoc API 文档、Bean Validation 校验
📝 作业
-
基础题(难度⭐):实现用户注册/登录和商品 CRUD 的完整代码,确保 JWT 认证和缓存生效。
-
进阶题(难度⭐⭐):实现订单模块(下单、取消、超时自动取消)和支付模块(发起支付、幂等回调),使用 Postman 完成端到端业务流程测试。
-
挑战题(难度⭐⭐⭐):为 OrderFlow 添加 SpringDoc API 文档,配置 Swagger UI 可交互测试;添加 Flyway 数据库迁移脚本;使用 TestContainers 编写核心流程的集成测试。