Spring Boot架构设计:从分层到领域驱动的可扩展实践
在技术领域,我们常常会探讨一些思想、理念或架构模式的前瞻性。今天,我们不谈具体的历史人物,而是聚焦于一种在软件开发中至关重要的思维方式——架构与设计的超前性。这种思维方式,往往能帮助我们在项目初期就规避掉未来可能出现的诸多技术债务和扩展性瓶颈。
无论是微服务架构的演进、领域驱动设计(DDD)的引入,还是云原生理念的普及,其核心思想都具有一定的超前性。它们并非为了解决当下某个具体Bug而生,而是为了应对未来业务的复杂性、团队的扩张以及技术的迭代。本文将从一个后端开发者的视角,深入探讨如何培养这种“超前”的设计思想,并通过一个Spring Boot微服务项目的实战案例,展示如何将超前思维落地为可维护、可扩展的代码结构。
本文适合有一定Spring Boot开发经验,希望提升系统设计能力、避免项目后期陷入重构泥潭的中高级开发者。我们将从设计原则讲起,过渡到具体的代码分层与模块化实践,最后给出一个包含完整代码的示例项目。
1. 理解“超前设计”的核心原则
在开始写代码之前,我们需要明确几个核心原则。这些原则是判断一个设计是否“超前”或者说是否“经得起时间考验”的标尺。
1.1 单一职责与高内聚(Single Responsibility & High Cohesion)
这是所有设计原则的基石。一个类、一个模块甚至一个服务,应该只有一个引起它变化的原因。超前设计会刻意地将不同的职责分离到不同的实体中,哪怕当前需求看起来很简单。
为什么这么做?因为需求一定会变化。今天用户信息只包含姓名和邮箱,明天可能就要加上手机号、头像、实名认证状态。如果一开始就把用户信息管理、用户认证、用户资料查询等逻辑全部塞在一个UserService里,随着需求膨胀,这个类会迅速变成几千行的“上帝类”,难以理解和维护。
超前思维体现:在需求只有“增删改查”时,就考虑将“数据持久化”、“业务逻辑”、“外部接口调用”进行分离。为未来的权限校验、日志审计、数据加密等横切关注点预留整合空间。
1.2 对扩展开放,对修改关闭(Open-Closed Principle)
模块应该对扩展开放,对修改关闭。这意味着,当需要增加新功能时,应通过增加新的代码(新类、新接口实现)来完成,而非修改已有的、已经测试通过的代码。
为什么这么做?直接修改核心逻辑风险极高,可能引发意想不到的副作用。超前设计会大量运用接口抽象和策略模式,将可能变化的部分封装起来。
示例:一个消息发送功能,最初只支持邮件。
// 初始设计 - 硬编码,不易扩展 public class NotificationService { public void sendNotification(String message) { // 直接调用邮件发送SDK EmailSender.send(message); } }超前设计会这样写:
// 1. 定义发送接口 public interface MessageSender { void send(String message); } // 2. 实现邮件发送 @Component public class EmailSender implements MessageSender { @Override public void send(String message) { // 发送邮件逻辑 System.out.println("Sending email: " + message); } } // 3. 服务类依赖接口,而非具体实现 @Service public class NotificationService { private final List<MessageSender> senders; // Spring会自动注入所有MessageSender的实现 public NotificationService(List<MessageSender> senders) { this.senders = senders; } public void notifyAll(String message) { senders.forEach(sender -> sender.send(message)); } }这样,未来需要添加短信、钉钉、微信推送时,只需新增实现MessageSender的类即可,NotificationService的代码一行都不用改。
1.3 依赖抽象而非具体(Dependency Inversion)
高层模块不应依赖低层模块,二者都应依赖其抽象。这直接通过Spring的依赖注入(DI)和面向接口编程来实现,是保证系统可测试性和可替换性的关键。
1.4 预见性的模块化与边界划分
超前设计会尝试在项目初期就根据业务领域(Domain)而非技术层级来划分模块。这就是领域驱动设计(DDD)的雏形。即使不全面实施DDD,也可以借鉴其“限界上下文”的思想,思考哪些功能应该紧密内聚,哪些应该松散耦合。
2. 环境准备与项目骨架
我们通过一个“用户订单中心”的简化案例来实践。假设我们有一个电商场景,需要管理用户和订单。
环境说明:
- JDK:17 或 21 (推荐17,长期支持版本)
- 构建工具:Maven 3.6+ 或 Gradle 7.x
- Spring Boot:3.1.x (本文示例基于3.1.5)
- IDE:IntelliJ IDEA 或 VS Code
- 数据库:示例使用H2内存数据库,方便演示。生产环境可替换为MySQL/PostgreSQL。
项目初始化:使用 Spring Initializr 生成项目,选择以下依赖:
- Spring Web
- Spring Data JPA
- H2 Database (或 MySQL Driver)
- Lombok (减少样板代码)
- Validation
生成后的pom.xml关键依赖如下:
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>3.1.5</version> <relativePath/> </parent> <groupId>com.example</groupId> <artifactId>advanced-design-demo</artifactId> <version>0.0.1-SNAPSHOT</version> <name>advanced-design-demo</name> <description>Demo project for advanced design thinking</description> <properties> <java.version>17</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-jpa</artifactId> </dependency> <dependency> <groupId>com.h2database</groupId> <artifactId>h2</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <optional>true</optional> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-validation</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> <configuration> <excludes> <exclude> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </exclude> </excludes> </configuration> </plugin> </plugins> </build> </project>3. 分层架构与模块化设计实战
很多教程会教你标准的Controller-Service-Repository三层架构。这没错,但超前设计需要在此基础上,思考每一层的职责边界和未来的变化点。
3.1 传统三层架构的潜在问题
传统的三层架构容易导致Service层变成“杂物间”,所有业务逻辑都堆在里面。随着业务复杂,OrderService可能既要处理订单创建、支付、发货,又要计算优惠券、积分,还要调用外部物流接口,最终变得臃肿不堪。
3.2 改进:领域模型与职责细分
我们引入更清晰的分层,并初步借鉴DDD的战术模式:
- 表示层 (Presentation Layer):
Controller,只负责接收请求、校验参数格式、返回响应。不包含任何业务逻辑。 - 应用层 (Application Layer):
Service(或ApplicationService),负责协调领域对象完成一个完整的业务用例(User Case),如“创建订单”。它本身不实现核心业务规则,而是调用领域层的服务。 - 领域层 (Domain Layer):这是业务核心。包含:
- 实体 (Entity):具有唯一标识和生命周期的业务对象,如
User,Order。 - 值对象 (Value Object):描述事物特征的无标识对象,如
Money,Address。 - 领域服务 (Domain Service):处理不属于单个实体/值对象的业务逻辑,如
OrderCreationService(涉及库存、用户、商品等多个实体)。 - 仓储接口 (Repository Interface):定义数据访问契约,实现在基础设施层。
- 实体 (Entity):具有唯一标识和生命周期的业务对象,如
- 基础设施层 (Infrastructure Layer):提供技术实现,如数据库访问(
JpaRepository实现)、消息队列客户端、外部API调用等。
3.3 项目结构设计
我们按照上述思想创建包结构。注意,这是按领域而非技术划分的顶层包。
src/main/java/com/example/advanceddesign/ ├── order/ # 订单领域 │ ├── application/ # 应用层 │ │ ├── OrderApplicationService.java │ │ └── dto/ # 应用层数据传输对象 │ │ ├── CreateOrderCommand.java │ │ └── OrderResultDTO.java │ ├── domain/ # 领域层 │ │ ├── model/ # 领域模型 │ │ │ ├── Order.java # 订单实体(聚合根) │ │ │ ├── OrderItem.java # 订单项值对象 │ │ │ └── OrderStatus.java # 枚举:订单状态 │ │ ├── service/ # 领域服务 │ │ │ └── OrderCreationDomainService.java │ │ └── repository/ # 仓储接口 │ │ └── OrderRepository.java │ └── infrastructure/ # 基础设施层(订单领域相关) │ └── persistence/ # 持久化实现 │ └── OrderRepositoryImpl.java (可选,JPA可直接用接口) ├── user/ # 用户领域(结构类似) │ ├── application/ │ ├── domain/ │ └── infrastructure/ ├── shared/ # 共享内核 │ ├── kernel/ # 通用领域概念,如Money │ ├── exception/ # 全局异常定义 │ └── web/ # 通用Web组件,如全局响应体 └── AdvancedDesignDemoApplication.java这种结构在项目初期看似复杂,但它强制你思考每个类的归属和职责。当“用户积分”和“订单折扣”逻辑耦合时,你能立刻发现并思考是否应该引入一个独立的“促销”领域。
4. 核心代码实现:从贫血模型到充血模型
传统开发模式容易产生“贫血模型”:实体类只有getter/setter和JPA注解,所有业务逻辑都在Service里。这违反了面向对象“数据与行为封装在一起”的原则。
超前设计追求“充血模型”:将属于该实体自身的业务逻辑封装在实体内部。
4.1 定义共享内核的值对象:Money
在shared.kernel包下创建Money值对象。处理金额永远不应该用BigDecimal裸奔,而应该封装。
package com.example.advanceddesign.shared.kernel; import jakarta.persistence.Embeddable; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; import java.math.BigDecimal; import java.util.Currency; @Embeddable // 表示可嵌入到其他实体中 @Getter @NoArgsConstructor @AllArgsConstructor public class Money { private BigDecimal amount; private Currency currency; // 业务行为:加法 public Money add(Money other) { if (!this.currency.equals(other.currency)) { throw new IllegalArgumentException("Cannot add different currencies"); } return new Money(this.amount.add(other.amount), this.currency); } // 业务行为:乘法(用于计算折扣等) public Money multiply(BigDecimal multiplier) { return new Money(this.amount.multiply(multiplier), this.currency); } // 业务行为:比较 public boolean isGreaterThan(Money other) { checkSameCurrency(other); return this.amount.compareTo(other.amount) > 0; } private void checkSameCurrency(Money other) { if (!this.currency.equals(other.currency)) { throw new IllegalArgumentException("Currency mismatch"); } } // 静态工厂方法,提供更清晰的创建语义 public static Money of(BigDecimal amount, Currency currency) { return new Money(amount, currency); } public static Money of(String amount, Currency currency) { return new Money(new BigDecimal(amount), currency); } }4.2 实现订单领域模型
在order.domain.model包下创建Order实体和OrderItem值对象。
OrderItem.java (值对象)
package com.example.advanceddesign.order.domain.model; import com.example.advanceddesign.shared.kernel.Money; import jakarta.persistence.*; import lombok.AllArgsConstructor; import lombok.Getter; import lombok.NoArgsConstructor; @Embeddable // 作为值对象嵌入Order @Getter @NoArgsConstructor @AllArgsConstructor public class OrderItem { private Long productId; private String productName; private Integer quantity; @Embedded @AttributeOverrides({ @AttributeOverride(name = "amount", column = @Column(name = "item_price_amount")), @AttributeOverride(name = "currency", column = @Column(name = "item_price_currency")) }) private Money unitPrice; // 单价 // 计算该订单项的总价 public Money calculateTotal() { return unitPrice.multiply(new BigDecimal(quantity)); } }Order.java (实体,聚合根)
package com.example.advanceddesign.order.domain.model; import com.example.advanceddesign.shared.kernel.Money; import jakarta.persistence.*; import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; import java.math.BigDecimal; import java.time.LocalDateTime; import java.util.ArrayList; import java.util.List; import java.util.UUID; @Entity @Table(name = "orders") @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) // JPA要求,也防止直接new public class Order { @Id @GeneratedValue(strategy = GenerationType.UUID) // 使用UUID,便于分布式系统 private UUID id; private Long userId; private LocalDateTime createdAt; @Enumerated(EnumType.STRING) private OrderStatus status; @Embedded @AttributeOverrides({ @AttributeOverride(name = "amount", column = @Column(name = "total_amount_amount")), @AttributeOverride(name = "currency", column = @Column(name = "total_amount_currency")) }) private Money totalAmount; // 一对多关系,OrderItem是值对象,使用@ElementCollection @ElementCollection(fetch = FetchType.EAGER) @CollectionTable(name = "order_items", joinColumns = @JoinColumn(name = "order_id")) private List<OrderItem> items = new ArrayList<>(); // 核心业务逻辑:创建订单(静态工厂方法,封装创建逻辑) public static Order create(Long userId, List<OrderItem> items) { Order order = new Order(); order.userId = userId; order.createdAt = LocalDateTime.now(); order.status = OrderStatus.CREATED; order.items = new ArrayList<>(items); // 防御性复制 // 计算总金额:业务逻辑内聚在实体内部 Money total = Money.of(BigDecimal.ZERO, items.get(0).getUnitPrice().getCurrency()); for (OrderItem item : items) { total = total.add(item.calculateTotal()); } order.totalAmount = total; // 未来可以在这里添加创建时的其他校验,如商品库存(需调用领域服务) // if (!inventoryService.isSufficient(...)) { throw new BusinessException(...); } return order; } // 核心业务逻辑:支付订单 public void pay() { if (this.status != OrderStatus.CREATED) { throw new IllegalStateException("Only orders in CREATED status can be paid."); } this.status = OrderStatus.PAID; // 可以触发领域事件:OrderPaidEvent,用于后续更新库存、发送通知等 // this.registerEvent(new OrderPaidEvent(this.id)); } // 核心业务逻辑:取消订单 public void cancel() { if (this.status == OrderStatus.SHIPPED || this.status == OrderStatus.DELIVERED) { throw new IllegalStateException("Shipped or delivered orders cannot be cancelled."); } this.status = OrderStatus.CANCELLED; } // 查询业务逻辑:判断订单是否属于某用户 public boolean isOwnedBy(Long userId) { return this.userId.equals(userId); } }注意:实体内部包含了状态流转的逻辑(pay(),cancel())。这比在Service里写if-else判断order.getStatus()要清晰和安全得多。
4.3 定义领域服务
有些业务逻辑涉及多个实体,不适合放在单个实体内部。例如,创建订单需要校验库存、用户状态等。我们将其放在领域服务中。
OrderCreationDomainService.java
package com.example.advanceddesign.order.domain.service; import com.example.advanceddesign.order.domain.model.Order; import com.example.advanceddesign.order.domain.model.OrderItem; import com.example.advanceddesign.user.domain.repository.UserRepository; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.util.List; @Service @RequiredArgsConstructor @Transactional // 领域服务通常需要事务 public class OrderCreationDomainService { // 依赖其他领域的仓储接口,注意是接口! private final UserRepository userRepository; // 假设有一个商品库存的领域服务接口 // private final InventoryService inventoryService; public Order createOrder(Long userId, List<OrderItem> items) { // 1. 校验用户是否存在且有效(调用用户领域) userRepository.findById(userId) .orElseThrow(() -> new IllegalArgumentException("User not found")); // 这里可以添加更复杂的用户状态校验,如是否被禁用 // 2. 校验库存(调用库存领域,这里简化) // inventoryService.checkStock(items); // 3. 调用实体工厂方法创建订单(核心业务逻辑在实体内) Order newOrder = Order.create(userId, items); // 4. 可能触发其他操作,如扣减库存(通过领域事件异步处理更佳) // inventoryService.reduceStock(items); return newOrder; } }4.4 应用层服务编排
应用层服务OrderApplicationService负责协调。它注入领域服务,并处理DTO转换、事务边界等应用层职责。
CreateOrderCommand.java (应用层入参)
package com.example.advanceddesign.order.application.dto; import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotNull; import lombok.Data; import java.util.List; @Data public class CreateOrderCommand { @NotNull private Long userId; @NotEmpty private List<OrderItemCommand> items; @Data public static class OrderItemCommand { @NotNull private Long productId; @NotNull private String productName; @NotNull private Integer quantity; @NotNull private String amount; // 金额字符串,如 "99.99" @NotNull private String currency; // 货币代码,如 "CNY" } }OrderApplicationService.java
package com.example.advanceddesign.order.application; import com.example.advanceddesign.order.application.dto.CreateOrderCommand; import com.example.advanceddesign.order.domain.model.OrderItem; import com.example.advanceddesign.order.domain.service.OrderCreationDomainService; import com.example.advanceddesign.order.domain.repository.OrderRepository; import com.example.advanceddesign.shared.kernel.Money; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import java.util.Currency; import java.util.stream.Collectors; @Service @RequiredArgsConstructor public class OrderApplicationService { private final OrderCreationDomainService orderCreationDomainService; private final OrderRepository orderRepository; public String createOrder(CreateOrderCommand command) { // 1. DTO 转换为 领域对象 List<OrderItem> orderItems = command.getItems().stream() .map(itemCmd -> new OrderItem( itemCmd.getProductId(), itemCmd.getProductName(), itemCmd.getQuantity(), Money.of(itemCmd.getAmount(), Currency.getInstance(itemCmd.getCurrency())) )) .collect(Collectors.toList()); // 2. 调用领域服务执行业务逻辑 var newOrder = orderCreationDomainService.createOrder(command.getUserId(), orderItems); // 3. 持久化(领域服务返回的是内存对象,由应用层决定何时保存) orderRepository.save(newOrder); // 4. 返回结果(可以是ID,也可以是更复杂的DTO) return newOrder.getId().toString(); } }4.5 表示层Controller
Controller保持简洁,只做参数校验和响应封装。
OrderController.java
package com.example.advanceddesign.order.interfaces.web; import com.example.advanceddesign.order.application.OrderApplicationService; import com.example.advanceddesign.order.application.dto.CreateOrderCommand; import com.example.advanceddesign.shared.web.ApiResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; @RestController @RequestMapping("/api/orders") @RequiredArgsConstructor public class OrderController { private final OrderApplicationService orderApplicationService; @PostMapping public ApiResponse<String> createOrder(@Valid @RequestBody CreateOrderCommand command) { String orderId = orderApplicationService.createOrder(command); return ApiResponse.success("Order created successfully", orderId); } }ApiResponse.java (通用响应体)
package com.example.advanceddesign.shared.web; import lombok.Data; @Data public class ApiResponse<T> { private boolean success; private String message; private T data; private long timestamp; private ApiResponse(boolean success, String message, T data) { this.success = success; this.message = message; this.data = data; this.timestamp = System.currentTimeMillis(); } public static <T> ApiResponse<T> success(String message, T data) { return new ApiResponse<>(true, message, data); } public static <T> ApiResponse<T> success(T data) { return success("Operation successful", data); } public static <T> ApiResponse<T> error(String message) { return new ApiResponse<>(false, message, null); } }5. 运行与验证
- 启动Spring Boot应用。
- 使用Postman或curl发送POST请求:
curl -X POST http://localhost:8080/api/orders \ -H "Content-Type: application/json" \ -d '{ "userId": 123, "items": [ { "productId": 1001, "productName": "Spring Boot实战", "quantity": 2, "amount": "59.99", "currency": "CNY" }, { "productId": 1002, "productName": "领域驱动设计", "quantity": 1, "amount": "89.99", "currency": "CNY" } ] }' - 预期响应:
{ "success": true, "message": "Order created successfully", "data": "550e8400-e29b-41d4-a716-446655440000", // 生成的UUID "timestamp": 1678881234567 } - 查看H2控制台 (
http://localhost:8080/h2-console,JDBC URL:jdbc:h2:mem:testdb),可以看到orders表和order_items表已生成,数据已持久化。
6. 超前设计带来的优势与常见问题
6.1 优势总结
- 高可维护性:业务逻辑集中在领域层,结构清晰。修改订单状态流转规则只需改
Order实体。 - 强可测试性:领域模型(如
Money,Order)不依赖Spring和数据库,可以轻松进行单元测试。领域服务也可以通过Mock接口进行测试。 - 易扩展性:需要新增支付方式、物流公司时,通过策略模式或新增领域服务实现,对原有代码影响极小。
- 技术细节隔离:数据库从H2换为MySQL,只需调整基础设施层的配置和方言,领域层和应用层代码无需改动。
6.2 常见问题与排查思路
| 问题现象 | 可能原因 | 解决思路 |
|---|---|---|
JPA无法保存Money或OrderItem | 未正确使用@Embeddable和@Embedded注解 | 检查值对象类是否有@Embeddable,实体中引用字段是否有@Embedded。对于集合,使用@ElementCollection。 |
| 领域服务中注入的Repository为null | 领域服务未被Spring管理,或包扫描路径不对 | 确保领域服务类有@Service或@Component注解,并位于主应用类能扫描到的包下。 |
| 修改实体状态后数据库未更新 | 在领域服务或应用层中修改了实体,但未调用Repository.save() | 确保在事务边界内(如应用层方法有@Transactional)并显式调用了save。或者使用JPA的“脏检查”机制(在事务内从Repository查出的实体,修改后事务提交会自动更新)。 |
| 觉得代码比传统三层架构“复杂” | 初期不熟悉领域驱动设计概念 | 从小模块开始实践。对于简单CRUD项目,传统三层架构足够。当业务逻辑超过5个if-else时,考虑引入领域模型封装逻辑。 |
| 领域服务变得臃肿 | 把本该属于实体的逻辑放到了领域服务 | 反复审视:这个操作是否只涉及一个实体?如果是,尽量移到实体内部。领域服务应协调多个实体或与外部系统交互。 |
7. 最佳实践与工程建议
- 渐进式演进:不要一开始就追求完美的DDD。可以从“富血模型”开始,把最核心、最复杂的业务逻辑封装到实体中。随着业务复杂度的提升,再逐步引入领域服务、聚合、限界上下文等概念。
- 依赖方向严格性:牢记依赖关系:表示层 -> 应用层 -> 领域层 <- 基础设施层。领域层是核心,它不应该依赖任何外层(特别是基础设施层)。这通过依赖倒置(在领域层定义接口,在基础设施层实现)来实现。
- 谨慎使用贫血模型:对于简单的数据载体(如查询结果DTO、配置类),使用贫血模型(只有数据)没问题。但对于核心业务实体,务必赋予其行为。
- 重视测试:超前设计的价值在变更时最能体现。建立完善的测试套件,特别是领域模型的单元测试,能极大增强重构和扩展的信心。
- 文档与沟通:新的代码结构需要团队共识。使用清晰的包名、类名,并结合必要的文档(如README、架构图)来解释模块职责和交互方式。
- 性能考量:
@ElementCollection在某些场景下可能有性能问题。对于复杂的值对象集合,评估是否需要用@OneToMany关联一个实体。这属于基础设施层的优化,不影响领域模型的设计。 - 事务边界:通常将事务声明在应用层服务(
@Transactional)。一个应用层方法代表一个业务用例,应保证其原子性。避免在领域层或基础设施层滥用@Transactional。
超前设计是一种思维习惯,它要求开发者在编码时多问一句:“这个功能未来会怎么变?我的设计能轻松应对这种变化吗?”通过将易变的部分抽象、将核心逻辑内聚、将层次职责分离,我们构建的系统才能更从容地应对未来的不确定性。本文的示例项目提供了一个起点,你可以在其基础上,尝试引入领域事件(Domain Events)来解耦支付成功后的库存扣减和消息通知,或者将用户模块完整实现,体会跨领域调用的方式。真正的掌握,始于动手实践。
