DDD 领域驱动设计:从战略到战术,终结微服务拆分的所有混乱

简介: 本文深入剖析微服务拆分困境,指出问题根源在于混淆技术边界与业务边界。提出DDD(领域驱动设计)作为破局之道:以战略设计(领域划分、统一语言、事件风暴、上下文映射)确定微服务合理边界;以战术设计(四层架构、聚合根、值对象等)保障领域模型内聚。结合电商订单域完整落地示例,揭示DDD本质是“先懂业务,再写代码”的设计思想。

前言

在微服务架构普及的今天,很多团队都陷入了拆分的困境:有人按技术层拆分,把controller、service、dao拆成独立服务;有人按数据库表拆分,一张表对应一个微服务;最终拆出来的系统不仅没有实现解耦,反而变成了“分布式单体”——一个需求要改多个服务,接口依赖错综复杂,分布式事务问题频发,维护成本指数级上升。

本质上,这些问题的根源在于:微服务拆分的依据错了。微服务的核心是“业务边界”,而非技术边界。而DDD(领域驱动设计),正是一套帮我们找到业务边界、从业务视角出发设计系统的完整方法论。它不是一套技术框架,而是一种以业务为核心的设计思想,能从根源上解决微服务拆分的混乱问题。

一、DDD的核心本质:先搞懂业务,再谈技术实现

很多人对DDD的第一印象是一堆晦涩的概念,比如限界上下文、聚合根、值对象等等,但DDD的本质其实非常简单:先把业务搞清楚,再用代码去贴合业务,而不是先写代码再让业务去适配技术实现

1.1 DDD解决的核心问题

软件系统的复杂度分为两种:

  • 技术复杂度:比如高并发、高可用、高性能、分布式等技术问题
  • 业务复杂度:比如复杂的业务规则、多变的需求、多角色协同、长流程业务等问题

DDD的核心目标,是解决业务复杂度。它通过一套完整的方法论,帮我们把复杂的业务拆解成可管理的模块,让系统的代码结构和业务逻辑完全对齐,即使需求频繁变更,系统也能保持良好的可维护性和扩展性。

1.2 DDD的两大核心阶段

DDD的设计分为两个核心阶段,这也是微服务拆分的完整流程:

  1. 战略设计:定边界。从全局业务视角出发,划分业务领域、定义统一语言、拆分限界上下文,最终确定微服务的拆分边界。这是DDD的核心,也是微服务拆分成败的关键。
  2. 战术设计:做落地。在单个限界上下文内部,设计领域模型,定义聚合、实体、值对象、领域服务等核心元素,完成代码的落地实现。

二、战略设计:微服务拆分的核心依据

战略设计是DDD的灵魂,也是微服务拆分的唯一正确依据。很多团队微服务拆分混乱,本质上是跳过了战略设计,直接凭感觉拆分服务。

2.1 第一步:领域划分,找到业务的核心

领域,就是我们要解决的业务问题的范围。比如电商系统的领域,就是整个电商交易的全流程;OA系统的领域,就是企业办公的全流程。

我们需要把整个业务领域,按照业务的职能和价值,拆分为三种不同类型的子域:

  • 核心域:企业的核心竞争力,能给企业带来差异化竞争优势和核心利润的业务模块。比如电商系统的交易域、营销域,是决定平台生死的核心。
  • 支撑域:支撑核心域运行的业务模块,没有核心域那么关键,但又是核心域必不可少的依赖。比如电商系统的商品域、用户域、库存域。
  • 通用域:多个子域都能复用的通用能力,没有明显的业务属性,一般可以用通用的第三方服务替代。比如权限域、日志域、监控域、文件存储域。

划分原则:核心域必须投入最多的资源,做最深入的业务建模;支撑域保证稳定可用;通用域尽量复用成熟方案,不要重复造轮子。

2.2 第二步:定义统一语言,消除业务与开发的鸿沟

统一语言(Ubiquitous Language),是DDD最容易被忽略,但又最基础的核心元素。它指的是,在一个限界上下文内,业务、产品、开发、测试所有角色,都使用同一套词汇来描述业务,每个词汇都有唯一、明确的业务含义。

举个最常见的反例:

  • 业务方说的“订单”,指的是用户下单的整个单据,包含商品、收货地址、支付信息、物流信息等所有内容
  • 开发理解的“订单”,只是数据库里订单表的一条记录,不包含订单项和物流信息

这种词汇含义的不一致,会导致需求理解偏差,最终开发出来的系统完全不符合业务预期,后续的需求变更也会变得异常困难。

落地方法

  1. 把业务中所有的核心词汇,整理成统一的词汇表,明确每个词汇的业务含义、适用范围
  2. 词汇表必须由业务方和开发团队共同确认,保证所有人的理解一致
  3. 代码里的类名、方法名、变量名,必须和统一语言完全一致,比如业务里叫“订单取消”,代码里就不能叫“订单关闭”
  4. 需求沟通、文档编写、代码开发,全程只能使用统一语言里的词汇

2.3 第三步:事件风暴,拆分限界上下文

限界上下文(Bounded Context),是DDD战略设计的核心输出,也是微服务拆分的核心依据。一个限界上下文,就是一个业务边界,对应一个微服务

限界上下文,定义了统一语言的适用范围。在同一个限界上下文内,统一语言是明确、无歧义的;不同的限界上下文里,同一个词汇可能有不同的含义。比如“商品”在商品域里,包含了商品的所有属性、分类、库存信息;而在订单域里,“商品”只需要SKU ID、名称、单价这几个核心信息,完全是两个不同的概念。

拆分限界上下文最实用、最落地的方法,就是事件风暴(Event Storming)。它是一种团队协作的建模方法,通过可视化的方式,让业务和开发一起梳理业务流程,拆分限界上下文。

事件风暴的完整步骤

  1. 梳理业务场景:先明确我们要梳理的业务范围,比如电商的下单全流程,明确业务目标、参与的角色、核心的业务流程。
  2. 收集领域事件:领域事件,是业务中已经发生的、对业务有影响的事情,命名格式统一为“XX已XX”,比如“订单已创建”、“订单已支付”、“库存已扣减”、“物流单已创建”。把所有的领域事件,按照业务发生的时间顺序排列。
  3. 定位触发事件的命令:命令,是触发领域事件的用户操作,命名格式为“XX”,比如“创建订单”触发“订单已创建”,“支付订单”触发“订单已支付”。每个领域事件,都对应一个触发它的命令。
  4. 找到发起命令的角色:明确每个命令是由谁发起的,比如用户、运营人员、系统定时任务。
  5. 关联聚合与实体:找到每个命令和事件对应的业务载体,也就是聚合,明确这个业务操作是围绕哪个业务对象进行的,这个业务对象包含哪些属性和规则。
  6. 划分限界上下文:把语义和业务紧密相关的聚合、事件、命令,放到同一个限界上下文里。划分的核心原则是高内聚、低耦合:同一个上下文内的业务逻辑高度相关,上下文之间的依赖尽量少。
  7. 确定上下文映射关系:明确不同限界上下文之间的交互方式,也就是上下文映射。

事件风暴落地示例:电商系统

我们以电商系统为例,通过事件风暴拆分出的限界上下文如下:

  • 用户域:负责用户的注册、登录、信息管理、等级权益等
  • 商品域:负责商品的管理、分类、SKU、价格等
  • 订单域:负责订单的创建、支付、发货、完成、取消等全生命周期管理
  • 库存域:负责商品库存的扣减、回补、盘点等
  • 支付域:负责支付渠道对接、支付流水管理、退款等
  • 物流域:负责物流单创建、物流轨迹跟踪、签收管理等
  • 营销域:负责优惠活动、优惠券、积分等
  • 权限域:负责用户权限、菜单管理、角色管理等

每个限界上下文,都对应一个独立的微服务。这就是基于DDD的微服务拆分,完全按照业务边界拆分,而不是技术边界。

2.4 第四步:上下文映射,处理微服务之间的交互

拆分完限界上下文之后,我们需要明确不同上下文之间的交互关系,也就是上下文映射(Context Mapping)。它决定了微服务之间的调用方式、解耦方案。

常见的上下文映射类型有以下7种,其中最常用的是防腐层、发布-订阅、客户方-供应方:

  1. 客户方-供应方(Customer-Supplier):两个上下文是上下游关系,客户方上下文依赖供应方上下文的能力,客户方提出需求,供应方提供接口。比如订单域是客户方,商品域是供应方,订单域需要调用商品域的接口查询商品信息。
  2. 防腐层(Anticorruption Layer,ACL):客户方上下文在自己的域内,定义一套适配接口,把供应方的接口模型转换成自己的领域模型,隔离供应方的变化。即使供应方的接口改了,只需要修改防腐层的适配逻辑,不需要修改自己的核心业务代码。这是跨微服务调用最常用的模式,能最大程度保证自己域的稳定性。
  3. 发布-订阅(Publish-Subscribe):一个上下文发布领域事件,其他感兴趣的上下文订阅这个事件,做出对应的处理。比如订单域发布“订单已创建”事件,库存域订阅这个事件扣减库存,物流域订阅这个事件创建物流单。这种模式能最大程度解耦微服务,避免同步调用的强依赖。
  4. 合作关系(Partnership):两个上下文的团队深度合作,共同制定接口规范,一起迭代。比如订单域和营销域,优惠计算和订单创建强相关,两个团队需要一起合作设计。
  5. 遵奉者(Conformist):客户方完全遵奉供应方的接口模型,不做任何转换,直接使用。这种模式耦合度高,只有在供应方的模型完全符合客户方需求,且非常稳定的情况下使用。
  6. 共享内核(Shared Kernel):两个上下文共享一部分核心模型和代码,减少重复开发。这种模式耦合度极高,尽量避免使用,除非是两个上下文完全由同一个团队维护。
  7. 各行其道(Separate Ways):两个上下文没有任何交互,各自独立实现自己的业务,完全解耦。

三、战术设计:限界上下文内的领域模型落地

战略设计确定了微服务的边界,战术设计就是在单个限界上下文内部,完成领域模型的设计和代码落地。战术设计的核心,是充血领域模型,把业务规则和逻辑封装到领域对象里,而不是散落在service层。

3.1 DDD的四层架构

DDD的代码落地,通常采用四层架构,每层有明确的职责,严格遵守依赖倒置原则,保证领域层的核心地位不被技术细节污染。

每层的职责如下:

  1. 用户接口层(Interfaces):负责和前端/外部系统交互,包含controller、DTO、参数校验、结果统一封装。只负责接收请求和返回结果,不包含任何业务逻辑。
  2. 应用层(Application):负责业务流程的编排和事务控制,只负责调用领域层的能力,按照业务流程组装步骤,不包含任何核心业务逻辑。比如下单流程:校验用户→创建订单→扣库存→支付→通知物流,应用层只负责编排这个流程,不做任何业务规则判断。
  3. 领域层(Domain):系统的核心,所有的业务规则、业务逻辑都在这里。包含领域模型、聚合、实体、值对象、领域服务、仓储接口、领域事件。领域层不依赖任何其他层,是完全独立的,不被任何技术细节污染。
  4. 基础设施层(Infrastructure):负责技术细节的实现,包含数据库、缓存、消息队列、远程调用等技术能力。实现领域层定义的仓储接口,给上层提供技术支撑,依赖领域层,而不是反过来。

3.2 战术设计的核心元素

3.2.1 实体(Entity)

实体是有唯一标识的业务对象,它的核心特征是:

  • 有全局唯一的标识,整个生命周期内,标识不变,即使其他属性都变了,只要标识不变,还是同一个实体
  • 有自己的生命周期和状态变化
  • 包含和自身相关的业务规则和逻辑

比如订单、用户、商品,都是实体,每个订单都有唯一的订单号,不管订单的状态、金额怎么变,订单号不变,还是同一个订单。

3.2.2 值对象(Value Object)

值对象是没有唯一标识的业务对象,它的核心特征是:

  • 没有唯一标识,靠属性值来判断是否相等,只要所有属性都一样,就是同一个值对象
  • 不可变,一旦创建,就不能修改它的属性,要修改的话,只能创建一个新的值对象替换它
  • 只包含数据,不包含生命周期,通常作为实体的属性存在

比如收货地址、订单金额、坐标,都是值对象。收货地址的省、市、区、详细地址都一样,就是同一个地址;如果修改了详细地址,就创建一个新的地址对象,而不是修改原来的对象。

JDK17的record类型,天生就符合值对象的特征,不可变、自动实现equals和hashCode,是值对象的最佳实现方式。

3.2.3 聚合(Aggregate)与聚合根(Aggregate Root)

聚合是一组相关的实体和值对象的集合,是保证数据一致性的最小单元。每个聚合都有一个唯一的入口,就是聚合根。

聚合的核心规则:

  1. 聚合根是聚合内的一个特殊实体,有全局唯一标识,是外部访问聚合的唯一入口,外部只能通过聚合根来访问聚合内部的对象,不能直接访问聚合内的其他实体。
  2. 聚合内的实体,只有聚合内的唯一标识,不需要全局唯一标识。
  3. 一个事务,只能修改一个聚合,保证聚合内的数据强一致性。聚合之间的数据一致性,通过最终一致性来保证。
  4. 聚合之间不能直接引用,只能通过聚合根的ID来关联。

举个例子,订单聚合:

  • 聚合根是Order(订单),有全局唯一的订单号
  • 聚合内包含OrderItem(订单项)实体、Address(收货地址)值对象、OrderAmount(订单金额)值对象、OrderStatus(订单状态)枚举
  • 外部不能直接修改OrderItem,只能通过Order的addItem、removeItem等方法来操作订单项,保证订单的总金额、商品数量的一致性
  • 订单创建、支付、取消等所有业务操作,都必须通过Order聚合根来执行,保证订单的状态流转符合业务规则

常见的错误:把每个数据库表都做成一个聚合根,导致聚合失去了保证数据一致性的作用,最终又回到了贫血模型的老路上。

3.2.4 领域服务(Domain Service)

领域服务是包含核心业务逻辑的服务,它处理的是跨多个聚合的业务逻辑,或者是不适合放在单个聚合/值对象里的业务逻辑。

领域服务的核心规则:

  1. 领域服务包含的是核心业务逻辑,不是流程编排。
  2. 只有当业务逻辑不属于任何一个聚合/值对象的时候,才放到领域服务里。
  3. 领域服务属于领域层,和聚合、实体是平等的。

比如订单金额的计算,需要结合订单项、运费、优惠活动、用户等级,跨了多个聚合,这个逻辑就适合放在订单领域服务里。

3.2.5 应用服务(Application Service)

应用服务属于应用层,只负责业务流程的编排和事务控制,不包含任何核心业务逻辑。它的核心职责是:

  1. 接收用户接口层的请求,调用领域层的聚合、领域服务,完成业务流程的编排。
  2. 负责事务的控制,保证流程的原子性。
  3. 不做任何业务规则的判断,所有业务规则都交给领域层处理。

领域服务和应用服务的核心区别:领域服务做业务决策,包含核心业务逻辑;应用服务做流程编排,不包含业务逻辑。这是DDD战术设计最容易混淆的点,很多人把业务逻辑写在应用服务里,导致领域模型变成了贫血模型。

3.2.6 仓储(Repository)

仓储是用来管理聚合的持久化的,它的核心规则:

  1. 仓储接口定义在领域层,面向领域模型,只提供聚合的保存、查询、更新、删除方法,不暴露任何数据库相关的细节。
  2. 仓储的实现类放在基础设施层,依赖ORM框架完成数据库操作,实现领域层的仓储接口。
  3. 一个聚合对应一个仓储,只有聚合根才有仓储,聚合内的其他实体不能有独立的仓储。

仓储和DAO的核心区别:DAO是面向数据库的,直接操作数据库表;仓储是面向领域的,对外提供聚合对象的持久化能力,封装了数据库操作的细节。领域层只依赖仓储接口,不依赖任何ORM框架,实现了业务和技术的解耦。

3.2.7 领域事件(Domain Event)

领域事件是领域内发生的、对业务有影响的事情,用来实现聚合之间、限界上下文之间的解耦。

领域事件的核心规则:

  1. 领域事件是已经发生的事情,命名格式为“XX已XX”,包含事件发生的时间、相关的聚合根ID、核心业务数据。
  2. 领域事件在聚合的业务方法执行完成后发布,保证事件的发布和聚合的修改在同一个事务里。
  3. 同一个限界上下文内的领域事件,可以用Spring的事件机制同步处理;跨限界上下文的领域事件,用消息中间件实现异步处理,保证最终一致性。

四、完整落地实例:订单域微服务实现

我们以电商系统的订单域为例,基于上面的DDD方法论,完成从战略设计到战术落地的完整代码实现。

4.1 项目基础信息

  • JDK版本:17
  • 项目管理:Maven
  • 核心框架:Spring Boot 3.2.4
  • 持久层框架:MyBatis Plus 3.5.7
  • 接口文档:Swagger3(springdoc 2.5.0)
  • 包名:com.jam.demo
  • 数据库:MySQL 8.0

4.2 项目结构

com.jam.demo
├── DddDemoApplication.java
├── application
│   └── service
│       ├── OrderAppService.java
│       └── ProductAclService.java
├── domain
│   ├── aggregate
│   │   └── Order.java
│   ├── entity
│   │   └── OrderItem.java
│   ├── event
│   │   └── OrderCreatedEvent.java
│   ├── repository
│   │   └── OrderRepository.java
│   ├── service
│   │   └── OrderDomainService.java
│   └── valueobject
│       ├── Address.java
│       ├── OrderAmount.java
│       └── OrderStatus.java
├── infrastructure
│   ├── config
│   │   ├── MyBatisPlusConfig.java
│   │   └── RestTemplateConfig.java
│   ├── mapper
│   │   ├── OrderItemMapper.java
│   │   └── OrderMapper.java
│   └── repository
│       └── OrderRepositoryImpl.java
└── interfaces
   ├── controller
   │   └── OrderController.java
   └── dto
       ├── OrderCreateDTO.java
       ├── OrderItemDTO.java
       ├── ProductSkuDTO.java
       └── ResultDTO.java

4.3 核心依赖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.2.4</version>
       <relativePath/>
   </parent>
   <groupId>com.jam</groupId>
   <artifactId>ddd-demo</artifactId>
   <version>0.0.1-SNAPSHOT</version>
   <name>ddd-demo</name>
   <description>DDD领域驱动设计示例项目</description>
   <properties>
       <java.version>17</java.version>
       <mybatis-plus.version>3.5.7</mybatis-plus.version>
       <lombok.version>1.18.30</lombok.version>
       <fastjson2.version>2.0.52</fastjson2.version>
       <springdoc.version>2.5.0</springdoc.version>
       <guava.version>33.1.0-jre</guava.version>
       <mysql.version>8.0.36</mysql.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-jdbc</artifactId>
       </dependency>
       <dependency>
           <groupId>org.springframework.boot</groupId>
           <artifactId>spring-boot-starter-validation</artifactId>
       </dependency>
       <dependency>
           <groupId>com.baomidou</groupId>
           <artifactId>mybatis-plus-boot-starter</artifactId>
           <version>${mybatis-plus.version}</version>
       </dependency>
       <dependency>
           <groupId>com.mysql</groupId>
           <artifactId>mysql-connector-j</artifactId>
           <version>${mysql.version}</version>
       </dependency>
       <dependency>
           <groupId>org.projectlombok</groupId>
           <artifactId>lombok</artifactId>
           <version>${lombok.version}</version>
           <scope>provided</scope>
       </dependency>
       <dependency>
           <groupId>com.alibaba.fastjson2</groupId>
           <artifactId>fastjson2</artifactId>
           <version>${fastjson2.version}</version>
       </dependency>
       <dependency>
           <groupId>org.springdoc</groupId>
           <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
           <version>${springdoc.version}</version>
       </dependency>
       <dependency>
           <groupId>com.google.guava</groupId>
           <artifactId>guava</artifactId>
           <version>${guava.version}</version>
       </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>

4.4 MySQL建表语句

CREATE TABLE `t_order` (
 `order_id` varchar(32) NOT NULL COMMENT '订单ID',
 `user_id` varchar(32) NOT NULL COMMENT '用户ID',
 `order_status` int NOT NULL COMMENT '订单状态:1-已创建,2-已支付,3-已发货,4-已完成,5-已取消,6-已退款',
 `total_amount` decimal(10,2) NOT NULL COMMENT '商品总金额',
 `pay_amount` decimal(10,2) NOT NULL COMMENT '支付金额',
 `freight_amount` decimal(10,2) NOT NULL COMMENT '运费金额',
 `discount_amount` decimal(10,2) NOT NULL COMMENT '优惠金额',
 `province` varchar(32) NOT NULL COMMENT '省份',
 `city` varchar(32) NOT NULL COMMENT '城市',
 `district` varchar(32) NOT NULL COMMENT '区县',
 `detail_address` varchar(255) NOT NULL COMMENT '详细地址',
 `receiver_name` varchar(32) NOT NULL COMMENT '收件人姓名',
 `receiver_phone` varchar(11) NOT NULL COMMENT '收件人电话',
 `remark` varchar(255) DEFAULT NULL COMMENT '订单备注',
 `pay_time` datetime DEFAULT NULL COMMENT '支付时间',
 `ship_time` datetime DEFAULT NULL COMMENT '发货时间',
 `complete_time` datetime DEFAULT NULL COMMENT '完成时间',
 `cancel_time` datetime DEFAULT NULL COMMENT '取消时间',
 `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
 `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
 `deleted` int NOT NULL DEFAULT '0' COMMENT '逻辑删除标识:0-未删除,1-已删除',
 PRIMARY KEY (`order_id`),
 KEY `idx_user_id` (`user_id`),
 KEY `idx_order_status` (`order_status`),
 KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='订单表';

CREATE TABLE `t_order_item` (
 `item_id` varchar(32) NOT NULL COMMENT '订单项ID',
 `order_id` varchar(32) NOT NULL COMMENT '订单ID',
 `sku_id` varchar(32) NOT NULL COMMENT '商品SKU ID',
 `sku_name` varchar(255) NOT NULL COMMENT '商品SKU名称',
 `price` decimal(10,2) NOT NULL COMMENT '商品单价',
 `quantity` int NOT NULL COMMENT '购买数量',
 `total_price` decimal(10,2) NOT NULL COMMENT '订单项总金额',
 `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
 `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
 `deleted` int NOT NULL DEFAULT '0' COMMENT '逻辑删除标识:0-未删除,1-已删除',
 PRIMARY KEY (`item_id`),
 KEY `idx_order_id` (`order_id`),
 KEY `idx_sku_id` (`sku_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='订单项表';

4.5 领域层核心代码

4.5.1 值对象

package com.jam.demo.domain.valueobject;

import org.springframework.util.StringUtils;

/**
* 订单状态枚举
* @author ken
*/

public enum OrderStatus {
   CREATED(1, "已创建"),
   PAID(2, "已支付"),
   SHIPPED(3, "已发货"),
   COMPLETED(4, "已完成"),
   CANCELLED(5, "已取消"),
   REFUNDED(6, "已退款");

   private final Integer code;
   private final String desc;

   OrderStatus(Integer code, String desc) {
       this.code = code;
       this.desc = desc;
   }

   public Integer getCode() {
       return code;
   }

   public String getDesc() {
       return desc;
   }
}

package com.jam.demo.domain.valueobject;

import org.springframework.util.StringUtils;

/**
* 收货地址值对象
* @author ken
*/

public record Address(
       String province,
       String city,
       String district,
       String detailAddress,
       String receiverName,
       String receiverPhone
)
{
   /**
    * 校验地址合法性
    * @return 校验结果
    */

   public boolean isValid() {
       return StringUtils.hasText(province)
               && StringUtils.hasText(city)
               && StringUtils.hasText(district)
               && StringUtils.hasText(detailAddress)
               && StringUtils.hasText(receiverName)
               && StringUtils.hasText(receiverPhone)
               && receiverPhone.matches("^1[3-9]\\d{9}$");
   }
}

package com.jam.demo.domain.valueobject;

import org.springframework.util.ObjectUtils;
import java.math.BigDecimal;

/**
* 订单金额值对象
* @author ken
*/

public record OrderAmount(
       BigDecimal totalAmount,
       BigDecimal payAmount,
       BigDecimal freightAmount,
       BigDecimal discountAmount
)
{
   /**
    * 校验金额合法性
    * @return 校验结果
    */

   public boolean isValid() {
       return ObjectUtils.isNotEmpty(totalAmount) && totalAmount.compareTo(BigDecimal.ZERO) >= 0
               && ObjectUtils.isNotEmpty(payAmount) && payAmount.compareTo(BigDecimal.ZERO) >= 0
               && ObjectUtils.isNotEmpty(freightAmount) && freightAmount.compareTo(BigDecimal.ZERO) >= 0
               && ObjectUtils.isNotEmpty(discountAmount) && discountAmount.compareTo(BigDecimal.ZERO) >= 0
               && payAmount.compareTo(totalAmount.add(freightAmount).subtract(discountAmount)) == 0;
   }
}

4.5.2 实体

package com.jam.demo.domain.entity;

import com.baomidou.mybatisplus.annotation.*;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.time.LocalDateTime;

/**
* 订单项实体
* @author ken
*/

@Getter
@TableName("t_order_item")
@Schema(description = "订单项实体")
public class OrderItem {
   @Schema(description = "订单项ID")
   @TableId(type = IdType.ASSIGN_ID)
   private String itemId;

   @Schema(description = "订单ID")
   private String orderId;

   @Schema(description = "商品SKU ID")
   private String skuId;

   @Schema(description = "商品SKU名称")
   private String skuName;

   @Schema(description = "商品单价")
   private BigDecimal price;

   @Schema(description = "购买数量")
   private Integer quantity;

   @Schema(description = "订单项总金额")
   private BigDecimal totalPrice;

   @Schema(description = "创建时间")
   @TableField(fill = FieldFill.INSERT)
   private LocalDateTime createTime;

   @Schema(description = "更新时间")
   @TableField(fill = FieldFill.INSERT_UPDATE)
   private LocalDateTime updateTime;

   @Schema(description = "逻辑删除标识")
   @TableLogic
   @TableField(fill = FieldFill.INSERT)
   private Integer deleted;

   /**
    * 构建订单项
    * @param skuId 商品SKU ID
    * @param skuName 商品SKU名称
    * @param price 商品单价
    * @param quantity 购买数量
    * @return 订单项实体
    */

   public static OrderItem create(String skuId, String skuName, BigDecimal price, Integer quantity) {
       if (!StringUtils.hasText(skuId)) {
           throw new IllegalArgumentException("商品SKU ID不能为空");
       }
       if (quantity == null || quantity <= 0) {
           throw new IllegalArgumentException("商品数量必须大于0");
       }
       if (price == null || price.compareTo(BigDecimal.ZERO) < 0) {
           throw new IllegalArgumentException("商品单价不能为负数");
       }
       OrderItem item = new OrderItem();
       item.skuId = skuId;
       item.skuName = skuName;
       item.price = price;
       item.quantity = quantity;
       item.totalPrice = price.multiply(BigDecimal.valueOf(quantity));
       return item;
   }

   /**
    * 绑定订单ID
    * @param orderId 订单ID
    */

   public void bindOrder(String orderId) {
       if (!StringUtils.hasText(orderId)) {
           throw new IllegalArgumentException("订单ID不能为空");
       }
       this.orderId = orderId;
   }
}

4.5.3 聚合根

package com.jam.demo.domain.aggregate;

import com.baomidou.mybatisplus.annotation.*;
import com.jam.demo.domain.entity.OrderItem;
import com.jam.demo.domain.valueobject.Address;
import com.jam.demo.domain.valueobject.OrderAmount;
import com.jam.demo.domain.valueobject.OrderStatus;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Getter;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;

/**
* 订单聚合根
* @author ken
*/

@Getter
@TableName("t_order")
@Schema(description = "订单聚合根")
public class Order {
   @Schema(description = "订单ID")
   @TableId(type = IdType.ASSIGN_ID)
   private String orderId;

   @Schema(description = "用户ID")
   private String userId;

   @Schema(description = "订单状态")
   private OrderStatus orderStatus;

   @Schema(description = "订单金额信息")
   @TableField(exist = false)
   private OrderAmount orderAmount;

   @Schema(description = "收货地址信息")
   @TableField(exist = false)
   private Address address;

   @Schema(description = "订单项列表")
   @TableField(exist = false)
   private List<OrderItem> itemList;

   @Schema(description = "订单备注")
   private String remark;

   @Schema(description = "支付时间")
   private LocalDateTime payTime;

   @Schema(description = "发货时间")
   private LocalDateTime shipTime;

   @Schema(description = "完成时间")
   private LocalDateTime completeTime;

   @Schema(description = "取消时间")
   private LocalDateTime cancelTime;

   @Schema(description = "创建时间")
   @TableField(fill = FieldFill.INSERT)
   private LocalDateTime createTime;

   @Schema(description = "更新时间")
   @TableField(fill = FieldFill.INSERT_UPDATE)
   private LocalDateTime updateTime;

   @Schema(description = "逻辑删除标识")
   @TableLogic
   @TableField(fill = FieldFill.INSERT)
   private Integer deleted;

   @TableField("total_amount")
   private BigDecimal totalAmount;
   @TableField("pay_amount")
   private BigDecimal payAmount;
   @TableField("freight_amount")
   private BigDecimal freightAmount;
   @TableField("discount_amount")
   private BigDecimal discountAmount;
   @TableField("province")
   private String province;
   @TableField("city")
   private String city;
   @TableField("district")
   private String district;
   @TableField("detail_address")
   private String detailAddress;
   @TableField("receiver_name")
   private String receiverName;
   @TableField("receiver_phone")
   private String receiverPhone;

   /**
    * 创建订单
    * @param userId 用户ID
    * @param address 收货地址
    * @param orderAmount 订单金额
    * @param itemList 订单项列表
    * @param remark 订单备注
    * @return 订单聚合根
    */

   public static Order create(String userId, Address address, OrderAmount orderAmount, List<OrderItem> itemList, String remark) {
       if (!StringUtils.hasText(userId)) {
           throw new IllegalArgumentException("用户ID不能为空");
       }
       if (address == null || !address.isValid()) {
           throw new IllegalArgumentException("收货地址不合法");
       }
       if (orderAmount == null || !orderAmount.isValid()) {
           throw new IllegalArgumentException("订单金额不合法");
       }
       if (CollectionUtils.isEmpty(itemList)) {
           throw new IllegalArgumentException("订单项列表不能为空");
       }
       Order order = new Order();
       order.userId = userId;
       order.address = address;
       order.orderAmount = orderAmount;
       order.itemList = itemList;
       order.remark = remark;
       order.orderStatus = OrderStatus.CREATED;
       order.createTime = LocalDateTime.now();
       itemList.forEach(item -> item.bindOrder(order.orderId));
       order.syncDbField();
       return order;
   }

   /**
    * 订单支付成功
    * @param payTime 支付时间
    */

   public void paySuccess(LocalDateTime payTime) {
       if (this.orderStatus != OrderStatus.CREATED) {
           throw new IllegalStateException("只有已创建状态的订单才能支付");
       }
       this.orderStatus = OrderStatus.PAID;
       this.payTime = payTime == null ? LocalDateTime.now() : payTime;
       this.syncDbField();
   }

   /**
    * 订单发货
    * @param shipTime 发货时间
    */

   public void ship(LocalDateTime shipTime) {
       if (this.orderStatus != OrderStatus.PAID) {
           throw new IllegalStateException("只有已支付状态的订单才能发货");
       }
       this.orderStatus = OrderStatus.SHIPPED;
       this.shipTime = shipTime == null ? LocalDateTime.now() : shipTime;
       this.syncDbField();
   }

   /**
    * 订单完成
    * @param completeTime 完成时间
    */

   public void complete(LocalDateTime completeTime) {
       if (this.orderStatus != OrderStatus.SHIPPED) {
           throw new IllegalStateException("只有已发货状态的订单才能完成");
       }
       this.orderStatus = OrderStatus.COMPLETED;
       this.completeTime = completeTime == null ? LocalDateTime.now() : completeTime;
       this.syncDbField();
   }

   /**
    * 取消订单
    * @param cancelTime 取消时间
    * @param cancelReason 取消原因
    */

   public void cancel(LocalDateTime cancelTime, String cancelReason) {
       if (this.orderStatus != OrderStatus.CREATED && this.orderStatus != OrderStatus.PAID) {
           throw new IllegalStateException("只有已创建或已支付状态的订单才能取消");
       }
       if (!StringUtils.hasText(cancelReason)) {
           throw new IllegalArgumentException("取消原因不能为空");
       }
       this.orderStatus = OrderStatus.CANCELLED;
       this.cancelTime = cancelTime == null ? LocalDateTime.now() : cancelTime;
       this.remark = StringUtils.hasText(this.remark) ? this.remark + ";取消原因:" + cancelReason : "取消原因:" + cancelReason;
       this.syncDbField();
   }

   /**
    * 同步值对象到数据库字段
    */

   public void syncDbField() {
       if (this.orderAmount != null) {
           this.totalAmount = this.orderAmount.totalAmount();
           this.payAmount = this.orderAmount.payAmount();
           this.freightAmount = this.orderAmount.freightAmount();
           this.discountAmount = this.orderAmount.discountAmount();
       }
       if (this.address != null) {
           this.province = this.address.province();
           this.city = this.address.city();
           this.district = this.address.district();
           this.detailAddress = this.address.detailAddress();
           this.receiverName = this.address.receiverName();
           this.receiverPhone = this.address.receiverPhone();
       }
   }

   /**
    * 从数据库字段加载值对象
    */

   public void loadValueObject() {
       this.orderAmount = new OrderAmount(this.totalAmount, this.payAmount, this.freightAmount, this.discountAmount);
       this.address = new Address(this.province, this.city, this.district, this.detailAddress, this.receiverName, this.receiverPhone);
   }

   /**
    * 设置订单项列表
    * @param itemList 订单项列表
    */

   public void setItemList(List<OrderItem> itemList) {
       this.itemList = itemList;
   }
}

4.5.4 领域事件

package com.jam.demo.domain.event;

import com.jam.demo.domain.aggregate.Order;
import lombok.Getter;
import java.time.LocalDateTime;

/**
* 订单创建领域事件
* @author ken
*/

@Getter
public class OrderCreatedEvent {
   private final String orderId;
   private final String userId;
   private final LocalDateTime createdTime;

   public OrderCreatedEvent(Order order) {
       this.orderId = order.getOrderId();
       this.userId = order.getUserId();
       this.createdTime = order.getCreateTime();
   }
}

4.5.5 仓储接口

package com.jam.demo.domain.repository;

import com.jam.demo.domain.aggregate.Order;
import java.util.Optional;

/**
* 订单仓储接口
* @author ken
*/

public interface OrderRepository {
   /**
    * 保存订单
    * @param order 订单聚合根
    * @return 保存结果
    */

   Order save(Order order);

   /**
    * 根据订单ID查询订单
    * @param orderId 订单ID
    * @return 订单聚合根
    */

   Optional<Order> findById(String orderId);

   /**
    * 更新订单
    * @param order 订单聚合根
    * @return 更新结果
    */

   Order update(Order order);
}

4.5.6 领域服务

package com.jam.demo.domain.service;

import com.jam.demo.domain.entity.OrderItem;
import com.jam.demo.domain.valueobject.OrderAmount;
import com.jam.demo.domain.aggregate.Order;
import com.jam.demo.domain.valueobject.OrderStatus;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.math.BigDecimal;
import java.util.List;

/**
* 订单领域服务
* @author ken
*/

@Slf4j
@Service
public class OrderDomainService {

   /**
    * 计算订单金额
    * @param itemList 订单项列表
    * @param freightAmount 运费金额
    * @param discountAmount 优惠金额
    * @return 订单金额值对象
    */

   public OrderAmount calculateOrderAmount(List<OrderItem> itemList, BigDecimal freightAmount, BigDecimal discountAmount) {
       if (CollectionUtils.isEmpty(itemList)) {
           throw new IllegalArgumentException("订单项列表不能为空");
       }
       BigDecimal totalAmount = itemList.stream()
               .map(OrderItem::getTotalPrice)
               .reduce(BigDecimal.ZERO, BigDecimal::add);
       BigDecimal payAmount = totalAmount.add(freightAmount).subtract(discountAmount);
       if (payAmount.compareTo(BigDecimal.ZERO) < 0) {
           payAmount = BigDecimal.ZERO;
       }
       return new OrderAmount(totalAmount, payAmount, freightAmount, discountAmount);
   }

   /**
    * 校验订单状态是否可取消
    * @param order 订单聚合根
    * @return 校验结果
    */

   public boolean isOrderCancellable(Order order) {
       return order.getOrderStatus() == OrderStatus.CREATED || order.getOrderStatus() == OrderStatus.PAID;
   }
}

4.6 基础设施层代码

4.6.1 Mapper接口

package com.jam.demo.infrastructure.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.domain.aggregate.Order;
import org.apache.ibatis.annotations.Mapper;

/**
* 订单Mapper接口
* @author ken
*/

@Mapper
public interface OrderMapper extends BaseMapper<Order> {
}

package com.jam.demo.infrastructure.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.jam.demo.domain.entity.OrderItem;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;

/**
* 订单项Mapper接口
* @author ken
*/

@Mapper
public interface OrderItemMapper extends BaseMapper<OrderItem> {
   /**
    * 批量插入订单项
    * @param itemList 订单项列表
    * @return 插入条数
    */

   int batchInsert(List<OrderItem> itemList);
}

4.6.2 Mapper XML文件

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.jam.demo.infrastructure.mapper.OrderItemMapper">
   <insert id="batchInsert" parameterType="java.util.List">
       INSERT INTO t_order_item (item_id, order_id, sku_id, sku_name, price, quantity, total_price, create_time, update_time, deleted)
       VALUES
       <foreach collection="list" item="item" separator=",">
           (#{item.itemId}, #{item.orderId}, #{item.skuId}, #{item.skuName}, #{item.price}, #{item.quantity}, #{item.totalPrice}, #{item.createTime}, #{item.updateTime}, #{item.deleted})
       </foreach>
   </insert>
</mapper>

4.6.3 仓储实现类

package com.jam.demo.infrastructure.repository;

import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.jam.demo.domain.aggregate.Order;
import com.jam.demo.domain.entity.OrderItem;
import com.jam.demo.domain.repository.OrderRepository;
import com.jam.demo.infrastructure.mapper.OrderItemMapper;
import com.jam.demo.infrastructure.mapper.OrderMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import org.springframework.util.CollectionUtils;
import org.springframework.util.ObjectUtils;
import java.util.List;
import java.util.Optional;

/**
* 订单仓储实现类
* @author ken
*/

@Repository
@RequiredArgsConstructor
public class OrderRepositoryImpl implements OrderRepository {

   private final OrderMapper orderMapper;
   private final OrderItemMapper orderItemMapper;

   @Override
   public Order save(Order order) {
       orderMapper.insert(order);
       if (!CollectionUtils.isEmpty(order.getItemList())) {
           orderItemMapper.batchInsert(order.getItemList());
       }
       return order;
   }

   @Override
   public Optional<Order> findById(String orderId) {
       Order order = orderMapper.selectById(orderId);
       if (ObjectUtils.isEmpty(order)) {
           return Optional.empty();
       }
       order.loadValueObject();
       LambdaQueryWrapper<OrderItem> queryWrapper = new LambdaQueryWrapper<OrderItem>()
               .eq(OrderItem::getOrderId, orderId);
       List<OrderItem> itemList = orderItemMapper.selectList(queryWrapper);
       order.setItemList(itemList);
       return Optional.of(order);
   }

   @Override
   public Order update(Order order) {
       orderMapper.updateById(order);
       return order;
   }
}

4.6.4 配置类

package com.jam.demo.infrastructure.config;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.OptimisticLockerInnerInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.apache.ibatis.reflection.MetaObject;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.time.LocalDateTime;

/**
* MyBatisPlus配置类
* @author ken
*/

@Configuration
public class MyBatisPlusConfig {

   @Bean
   public MybatisPlusInterceptor mybatisPlusInterceptor() {
       MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
       interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
       interceptor.addInnerInterceptor(new OptimisticLockerInnerInterceptor());
       return interceptor;
   }

   @Bean
   public MetaObjectHandler metaObjectHandler() {
       return new MetaObjectHandler() {
           @Override
           public void insertFill(MetaObject metaObject) {
               this.strictInsertFill(metaObject, "createTime", LocalDateTime.class, LocalDateTime.now());
               this.strictInsertFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
               this.strictInsertFill(metaObject, "deleted", Integer.class, 0);
           }

           @Override
           public void updateFill(MetaObject metaObject) {
               this.strictUpdateFill(metaObject, "updateTime", LocalDateTime.class, LocalDateTime.now());
           }
       };
   }
}

package com.jam.demo.infrastructure.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

/**
* RestTemplate配置类
* @author ken
*/

@Configuration
public class RestTemplateConfig {

   @Bean
   public RestTemplate restTemplate() {
       return new RestTemplate();
   }
}

4.7 应用层代码

4.7.1 应用服务

package com.jam.demo.application.service;

import com.jam.demo.domain.aggregate.Order;
import com.jam.demo.domain.entity.OrderItem;
import com.jam.demo.domain.event.OrderCreatedEvent;
import com.jam.demo.domain.repository.OrderRepository;
import com.jam.demo.domain.service.OrderDomainService;
import com.jam.demo.domain.valueobject.Address;
import com.jam.demo.domain.valueobject.OrderAmount;
import com.jam.demo.interfaces.dto.OrderCreateDTO;
import com.jam.demo.interfaces.dto.ProductSkuDTO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;
import java.util.List;
import java.util.Optional;

/**
* 订单应用服务
* @author ken
*/

@Slf4j
@Service
@RequiredArgsConstructor
public class OrderAppService {

   private final OrderDomainService orderDomainService;
   private final OrderRepository orderRepository;
   private final TransactionTemplate transactionTemplate;
   private final ApplicationEventPublisher eventPublisher;
   private final ProductAclService productAclService;

   /**
    * 创建订单
    * @param dto 订单创建DTO
    * @return 订单ID
    */

   public String createOrder(OrderCreateDTO dto) {
       List<OrderItem> itemList = dto.getItemList().stream()
               .map(itemDTO -> {
                   ProductSkuDTO skuDTO = productAclService.getSkuById(itemDTO.getSkuId());
                   return OrderItem.create(itemDTO.getSkuId(), skuDTO.getSkuName(), skuDTO.getPrice(), itemDTO.getQuantity());
               })
               .toList();

       OrderAmount orderAmount = orderDomainService.calculateOrderAmount(
               itemList,
               dto.getFreightAmount(),
               dto.getDiscountAmount()
       );

       Address address = new Address(
               dto.getProvince(),
               dto.getCity(),
               dto.getDistrict(),
               dto.getDetailAddress(),
               dto.getReceiverName(),
               dto.getReceiverPhone()
       );

       Order order = transactionTemplate.execute(status -> {
           try {
               Order newOrder = Order.create(dto.getUserId(), address, orderAmount, itemList, dto.getRemark());
               orderRepository.save(newOrder);
               return newOrder;
           } catch (Exception e) {
               status.setRollbackOnly();
               log.error("创建订单失败", e);
               throw new RuntimeException("创建订单失败:" + e.getMessage(), e);
           }
       });

       eventPublisher.publishEvent(new OrderCreatedEvent(order));
       log.info("订单创建成功,订单ID:{}", order.getOrderId());
       return order.getOrderId();
   }

   /**
    * 订单支付成功
    * @param orderId 订单ID
    */

   public void paySuccess(String orderId) {
       Optional<Order> orderOptional = orderRepository.findById(orderId);
       if (orderOptional.isEmpty()) {
           throw new IllegalArgumentException("订单不存在");
       }
       Order order = orderOptional.get();

       transactionTemplate.execute(status -> {
           try {
               order.paySuccess(null);
               orderRepository.update(order);
               return null;
           } catch (Exception e) {
               status.setRollbackOnly();
               log.error("订单支付状态更新失败,订单ID:{}", orderId, e);
               throw new RuntimeException("订单支付状态更新失败:" + e.getMessage(), e);
           }
       });
   }

   /**
    * 取消订单
    * @param orderId 订单ID
    * @param cancelReason 取消原因
    */

   public void cancelOrder(String orderId, String cancelReason) {
       Optional<Order> orderOptional = orderRepository.findById(orderId);
       if (orderOptional.isEmpty()) {
           throw new IllegalArgumentException("订单不存在");
       }
       Order order = orderOptional.get();

       if (!orderDomainService.isOrderCancellable(order)) {
           throw new IllegalStateException("当前订单状态不支持取消");
       }

       transactionTemplate.execute(status -> {
           try {
               order.cancel(null, cancelReason);
               orderRepository.update(order);
               return null;
           } catch (Exception e) {
               status.setRollbackOnly();
               log.error("订单取消失败,订单ID:{}", orderId, e);
               throw new RuntimeException("订单取消失败:" + e.getMessage(), e);
           }
       });
   }

   /**
    * 根据订单ID查询订单
    * @param orderId 订单ID
    * @return 订单聚合根
    */

   public Order getOrderById(String orderId) {
       return orderRepository.findById(orderId)
               .orElseThrow(() -> new IllegalArgumentException("订单不存在"));
   }
}

4.7.2 防腐层服务

package com.jam.demo.application.service;

import com.alibaba.fastjson2.JSON;
import com.jam.demo.interfaces.dto.ProductSkuDTO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.client.RestTemplate;

/**
* 商品域防腐层服务
* @author ken
*/

@Slf4j
@Service
@RequiredArgsConstructor
public class ProductAclService {

   private final RestTemplate restTemplate;
   private static final String PRODUCT_SERVICE_URL = "http://product-service/sku/";

   /**
    * 根据SKU ID查询商品信息
    * @param skuId 商品SKU ID
    * @return 商品SKU DTO
    */

   public ProductSkuDTO getSkuById(String skuId) {
       if (!StringUtils.hasText(skuId)) {
           throw new IllegalArgumentException("商品SKU ID不能为空");
       }
       try {
           String url = PRODUCT_SERVICE_URL + skuId;
           String result = restTemplate.getForObject(url, String.class);
           ProductSkuDTO skuDTO = JSON.parseObject(result, ProductSkuDTO.class);
           if (skuDTO == null || !StringUtils.hasText(skuDTO.getSkuId())) {
               throw new RuntimeException("商品信息不存在");
           }
           return skuDTO;
       } catch (Exception e) {
           log.error("查询商品信息失败,SKU ID:{}", skuId, e);
           throw new RuntimeException("查询商品信息失败:" + e.getMessage(), e);
       }
   }
}

4.8 用户接口层代码

4.8.1 DTO类

package com.jam.demo.interfaces.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;

/**
* 订单创建DTO
* @author ken
*/

@Data
@Schema(description = "订单创建请求参数")
public class OrderCreateDTO {
   @Schema(description = "用户ID", requiredMode = Schema.RequiredMode.REQUIRED)
   private String userId;

   @Schema(description = "省份", requiredMode = Schema.RequiredMode.REQUIRED)
   private String province;

   @Schema(description = "城市", requiredMode = Schema.RequiredMode.REQUIRED)
   private String city;

   @Schema(description = "区县", requiredMode = Schema.RequiredMode.REQUIRED)
   private String district;

   @Schema(description = "详细地址", requiredMode = Schema.RequiredMode.REQUIRED)
   private String detailAddress;

   @Schema(description = "收件人姓名", requiredMode = Schema.RequiredMode.REQUIRED)
   private String receiverName;

   @Schema(description = "收件人电话", requiredMode = Schema.RequiredMode.REQUIRED)
   private String receiverPhone;

   @Schema(description = "运费金额", requiredMode = Schema.RequiredMode.REQUIRED)
   private BigDecimal freightAmount;

   @Schema(description = "优惠金额", requiredMode = Schema.RequiredMode.REQUIRED)
   private BigDecimal discountAmount;

   @Schema(description = "订单项列表", requiredMode = Schema.RequiredMode.REQUIRED)
   private List<OrderItemDTO> itemList;

   @Schema(description = "订单备注")
   private String remark;
}

package com.jam.demo.interfaces.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;

/**
* 订单项DTO
* @author ken
*/

@Data
@Schema(description = "订单项请求参数")
public class OrderItemDTO {
   @Schema(description = "商品SKU ID", requiredMode = Schema.RequiredMode.REQUIRED)
   private String skuId;

   @Schema(description = "购买数量", requiredMode = Schema.RequiredMode.REQUIRED)
   private Integer quantity;
}

package com.jam.demo.interfaces.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;

/**
* 商品SKU DTO
* @author ken
*/

@Data
@Schema(description = "商品SKU信息")
public class ProductSkuDTO {
   @Schema(description = "商品SKU ID")
   private String skuId;

   @Schema(description = "商品SKU名称")
   private String skuName;

   @Schema(description = "商品单价")
   private BigDecimal price;
}

package com.jam.demo.interfaces.dto;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;

/**
* 统一返回结果DTO
* @author ken
*/

@Data
@Schema(description = "统一返回结果")
public class ResultDTO<T> {
   @Schema(description = "响应码")
   private Integer code;
   @Schema(description = "响应消息")
   private String message;
   @Schema(description = "响应数据")
   private T data;

   private static final Integer SUCCESS_CODE = 200;
   private static final Integer ERROR_CODE = 500;
   private static final String SUCCESS_MESSAGE = "操作成功";
   private static final String ERROR_MESSAGE = "操作失败";

   public static <T> ResultDTO<T> success(T data) {
       ResultDTO<T> result = new ResultDTO<>();
       result.setCode(SUCCESS_CODE);
       result.setMessage(SUCCESS_MESSAGE);
       result.setData(data);
       return result;
   }

   public static <T> ResultDTO<T> success() {
       ResultDTO<T> result = new ResultDTO<>();
       result.setCode(SUCCESS_CODE);
       result.setMessage(SUCCESS_MESSAGE);
       return result;
   }

   public static <T> ResultDTO<T> error(String message) {
       ResultDTO<T> result = new ResultDTO<>();
       result.setCode(ERROR_CODE);
       result.setMessage(message);
       return result;
   }
}

4.8.2 Controller

package com.jam.demo.interfaces.controller;

import com.jam.demo.application.service.OrderAppService;
import com.jam.demo.domain.aggregate.Order;
import com.jam.demo.interfaces.dto.OrderCreateDTO;
import com.jam.demo.interfaces.dto.ResultDTO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

/**
* 订单控制器
* @author ken
*/

@RestController
@RequestMapping("/order")
@RequiredArgsConstructor
@Tag(name = "订单管理", description = "订单相关接口")
public class OrderController {

   private final OrderAppService orderAppService;

   @PostMapping("/create")
   @Operation(summary = "创建订单", description = "创建新的订单")
   public ResultDTO<String> createOrder(@RequestBody OrderCreateDTO dto) {
       String orderId = orderAppService.createOrder(dto);
       return ResultDTO.success(orderId);
   }

   @PostMapping("/paySuccess/{orderId}")
   @Operation(summary = "订单支付成功回调", description = "更新订单支付状态")
   public ResultDTO<Void> paySuccess(
           @Parameter(description = "订单ID", required = true)

           @PathVariable String orderId
   ) {
       orderAppService.paySuccess(orderId);
       return ResultDTO.success();
   }

   @PostMapping("/cancel/{orderId}")
   @Operation(summary = "取消订单", description = "取消已创建或已支付的订单")
   public ResultDTO<Void> cancelOrder(
           @Parameter(description = "订单ID", required = true)

           @PathVariable String orderId,
           @Parameter(description = "取消原因", required = true)
           @RequestParam String cancelReason
   ) {
       orderAppService.cancelOrder(orderId, cancelReason);
       return ResultDTO.success();
   }

   @GetMapping("/detail/{orderId}")
   @Operation(summary = "查询订单详情", description = "根据订单ID查询订单详情")
   public ResultDTO<Order> getOrderDetail(
           @Parameter(description = "订单ID", required = true)

           @PathVariable String orderId
   ) {
       Order order = orderAppService.getOrderById(orderId);
       return ResultDTO.success(order);
   }
}

4.9 启动类与配置文件

package com.jam.demo;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
* 项目启动类
* @author ken
*/

@SpringBootApplication
@MapperScan("com.jam.demo.infrastructure.mapper")
public class DddDemoApplication {

   public static void main(String[] args) {
       SpringApplication.run(DddDemoApplication.class, args);
   }
}

spring:
 application:
   name: ddd-demo
 datasource:
   url: jdbc:mysql://localhost:3306/ddd_demo?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
   username: root
   password: root
   driver-class-name: com.mysql.cj.jdbc.Driver
 jackson:
   default-property-inclusion: non_null
   serialization:
     write-dates-as-timestamps: false
   deserialization:
     fail-on-unknown-properties: false

mybatis-plus:
 mapper-locations: classpath:mapper/*.xml
 type-aliases-package: com.jam.demo.domain
 configuration:
   map-underscore-to-camel-case: true
   cache-enabled: false
   log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
 global-config:
   db-config:
     id-type: assign_id
     logic-delete-field: deleted
     logic-delete-value: 1
     logic-not-delete-value: 0

springdoc:
 swagger-ui:
   path: /swagger-ui.html
   tags-sorter: alpha
   operations-sorter: alpha
 api-docs:
   path: /v3/api-docs
 packages-to-scan: com.jam.demo.interfaces.controller

server:
 port: 8080

五、DDD落地的常见坑与避坑指南

5.1 为了DDD而DDD

DDD不是银弹,它只适合业务复杂度高、需求变化频繁的系统。如果你的系统只是简单的CRUD,业务逻辑非常简单,用DDD只会增加系统的复杂度,得不偿失。

5.2 跳过战略设计,直接做战术实现

很多人学DDD,只学了聚合根、实体这些战术概念,直接开始写代码,却跳过了最核心的战略设计。没有限界上下文的边界,没有统一语言,最终写出来的代码还是传统的MVC三层架构,只是换了个类名,完全没有发挥DDD的价值。

5.3 过度拆分微服务

很多人把一个限界上下文拆成多个微服务,导致服务间的调用关系错综复杂,分布式事务问题频发,最终变成了“分布式单体”。记住:一个限界上下文对应一个微服务,不要过度拆分。如果业务初期复杂度不高,甚至可以多个限界上下文放在同一个微服务里,随着业务发展再逐步拆分。

5.4 贫血模型,把业务逻辑写在应用服务里

这是最常见的错误。很多人用了DDD,但是实体只有get/set方法,所有的业务逻辑都写在应用服务里,领域模型完全没有业务逻辑,变成了贫血模型。记住:和单个聚合相关的业务逻辑,一定要放在聚合根里;跨聚合的业务逻辑,放在领域服务里;应用服务只做流程编排,不包含任何业务逻辑

5.5 每个表对应一个聚合根

聚合是保证数据一致性的最小单元,不是每个数据库表对应一个聚合根。如果每个表都做成一个聚合根,就失去了聚合保证数据一致性的作用,最终还是会回到传统的DAO操作的老路上。

5.6 多个微服务共享数据库

微服务的核心是自治,每个微服务必须自己管理自己的数据,不能共享数据库。如果多个微服务共享同一个数据库,一个微服务修改了表结构,其他所有微服务都会受影响,耦合度极高,完全违背了微服务的设计原则。微服务之间只能通过接口或者事件来交互,不能直接操作对方的数据库。

5.7 忽略统一语言

统一语言是DDD的基础,如果业务和开发用的词汇不一致,需求理解就会出现偏差,最终开发出来的系统完全不符合业务预期。统一语言不是一次性的工作,需要在整个项目生命周期里持续维护,所有的沟通、文档、代码都必须使用统一语言。

六、总结

DDD的核心,从来都不是那些晦涩的概念,而是一种以业务为核心的设计思想。它要求我们先放下技术,先把业务搞清楚,找到业务的边界,再用代码去贴合业务,而不是反过来,先写代码再让业务去适配技术。

微服务拆分的本质,是业务边界的拆分,而不是技术边界的拆分。DDD的战略设计,帮我们找到业务的边界,确定微服务的拆分依据;DDD的战术设计,帮我们把业务逻辑封装到领域模型里,让系统的代码结构和业务逻辑完全对齐,即使需求频繁变更,系统也能保持良好的可维护性和扩展性。

目录
相关文章
|
2天前
|
人工智能 JSON 机器人
让龙虾成为你的“公众号分身” | 阿里云服务器玩Openclaw
本文带你零成本玩转OpenClaw:学生认证白嫖6个月阿里云服务器,手把手配置飞书机器人、接入免费/高性价比AI模型(NVIDIA/通义),并打造微信公众号“全自动分身”——实时抓热榜、AI选题拆解、一键发布草稿,5分钟完成热点→文章全流程!
10320 35
让龙虾成为你的“公众号分身” | 阿里云服务器玩Openclaw
|
14天前
|
人工智能 安全 Linux
【OpenClaw保姆级图文教程】阿里云/本地部署集成模型Ollama/Qwen3.5/百炼 API 步骤流程及避坑指南
2026年,AI代理工具的部署逻辑已从“单一云端依赖”转向“云端+本地双轨模式”。OpenClaw(曾用名Clawdbot)作为开源AI代理框架,既支持对接阿里云百炼等云端免费API,也能通过Ollama部署本地大模型,完美解决两类核心需求:一是担心云端API泄露核心数据的隐私安全诉求;二是频繁调用导致token消耗过高的成本控制需求。
5964 14
|
22天前
|
人工智能 JavaScript Ubuntu
5分钟上手龙虾AI!OpenClaw部署(阿里云+本地)+ 免费多模型配置保姆级教程(MiniMax、Claude、阿里云百炼)
OpenClaw(昵称“龙虾AI”)作为2026年热门的开源个人AI助手,由PSPDFKit创始人Peter Steinberger开发,核心优势在于“真正执行任务”——不仅能聊天互动,还能自动处理邮件、管理日程、订机票、写代码等,且所有数据本地处理,隐私完全可控。它支持接入MiniMax、Claude、GPT等多类大模型,兼容微信、Telegram、飞书等主流聊天工具,搭配100+可扩展技能,成为兼顾实用性与隐私性的AI工具首选。
23347 121
|
8天前
|
人工智能 JavaScript API
解放双手!OpenClaw Agent Browser全攻略(阿里云+本地部署+免费API+网页自动化场景落地)
“让AI聊聊天、写代码不难,难的是让它自己打开网页、填表单、查数据”——2026年,无数OpenClaw用户被这个痛点困扰。参考文章直击核心:当AI只能“纸上谈兵”,无法实际操控浏览器,就永远成不了真正的“数字员工”。而Agent Browser技能的出现,彻底打破了这一壁垒——它给OpenClaw装上“上网的手和眼睛”,让AI能像真人一样打开网页、点击按钮、填写表单、提取数据,24小时不间断完成网页自动化任务。
2030 4