前言
在微服务架构普及的今天,很多团队都陷入了拆分的困境:有人按技术层拆分,把controller、service、dao拆成独立服务;有人按数据库表拆分,一张表对应一个微服务;最终拆出来的系统不仅没有实现解耦,反而变成了“分布式单体”——一个需求要改多个服务,接口依赖错综复杂,分布式事务问题频发,维护成本指数级上升。
本质上,这些问题的根源在于:微服务拆分的依据错了。微服务的核心是“业务边界”,而非技术边界。而DDD(领域驱动设计),正是一套帮我们找到业务边界、从业务视角出发设计系统的完整方法论。它不是一套技术框架,而是一种以业务为核心的设计思想,能从根源上解决微服务拆分的混乱问题。
一、DDD的核心本质:先搞懂业务,再谈技术实现
很多人对DDD的第一印象是一堆晦涩的概念,比如限界上下文、聚合根、值对象等等,但DDD的本质其实非常简单:先把业务搞清楚,再用代码去贴合业务,而不是先写代码再让业务去适配技术实现。
1.1 DDD解决的核心问题
软件系统的复杂度分为两种:
- 技术复杂度:比如高并发、高可用、高性能、分布式等技术问题
- 业务复杂度:比如复杂的业务规则、多变的需求、多角色协同、长流程业务等问题
DDD的核心目标,是解决业务复杂度。它通过一套完整的方法论,帮我们把复杂的业务拆解成可管理的模块,让系统的代码结构和业务逻辑完全对齐,即使需求频繁变更,系统也能保持良好的可维护性和扩展性。
1.2 DDD的两大核心阶段
DDD的设计分为两个核心阶段,这也是微服务拆分的完整流程:
- 战略设计:定边界。从全局业务视角出发,划分业务领域、定义统一语言、拆分限界上下文,最终确定微服务的拆分边界。这是DDD的核心,也是微服务拆分成败的关键。
- 战术设计:做落地。在单个限界上下文内部,设计领域模型,定义聚合、实体、值对象、领域服务等核心元素,完成代码的落地实现。
二、战略设计:微服务拆分的核心依据
战略设计是DDD的灵魂,也是微服务拆分的唯一正确依据。很多团队微服务拆分混乱,本质上是跳过了战略设计,直接凭感觉拆分服务。
2.1 第一步:领域划分,找到业务的核心
领域,就是我们要解决的业务问题的范围。比如电商系统的领域,就是整个电商交易的全流程;OA系统的领域,就是企业办公的全流程。
我们需要把整个业务领域,按照业务的职能和价值,拆分为三种不同类型的子域:
- 核心域:企业的核心竞争力,能给企业带来差异化竞争优势和核心利润的业务模块。比如电商系统的交易域、营销域,是决定平台生死的核心。
- 支撑域:支撑核心域运行的业务模块,没有核心域那么关键,但又是核心域必不可少的依赖。比如电商系统的商品域、用户域、库存域。
- 通用域:多个子域都能复用的通用能力,没有明显的业务属性,一般可以用通用的第三方服务替代。比如权限域、日志域、监控域、文件存储域。
划分原则:核心域必须投入最多的资源,做最深入的业务建模;支撑域保证稳定可用;通用域尽量复用成熟方案,不要重复造轮子。
2.2 第二步:定义统一语言,消除业务与开发的鸿沟
统一语言(Ubiquitous Language),是DDD最容易被忽略,但又最基础的核心元素。它指的是,在一个限界上下文内,业务、产品、开发、测试所有角色,都使用同一套词汇来描述业务,每个词汇都有唯一、明确的业务含义。
举个最常见的反例:
- 业务方说的“订单”,指的是用户下单的整个单据,包含商品、收货地址、支付信息、物流信息等所有内容
- 开发理解的“订单”,只是数据库里订单表的一条记录,不包含订单项和物流信息
这种词汇含义的不一致,会导致需求理解偏差,最终开发出来的系统完全不符合业务预期,后续的需求变更也会变得异常困难。
落地方法:
- 把业务中所有的核心词汇,整理成统一的词汇表,明确每个词汇的业务含义、适用范围
- 词汇表必须由业务方和开发团队共同确认,保证所有人的理解一致
- 代码里的类名、方法名、变量名,必须和统一语言完全一致,比如业务里叫“订单取消”,代码里就不能叫“订单关闭”
- 需求沟通、文档编写、代码开发,全程只能使用统一语言里的词汇
2.3 第三步:事件风暴,拆分限界上下文
限界上下文(Bounded Context),是DDD战略设计的核心输出,也是微服务拆分的核心依据。一个限界上下文,就是一个业务边界,对应一个微服务。
限界上下文,定义了统一语言的适用范围。在同一个限界上下文内,统一语言是明确、无歧义的;不同的限界上下文里,同一个词汇可能有不同的含义。比如“商品”在商品域里,包含了商品的所有属性、分类、库存信息;而在订单域里,“商品”只需要SKU ID、名称、单价这几个核心信息,完全是两个不同的概念。
拆分限界上下文最实用、最落地的方法,就是事件风暴(Event Storming)。它是一种团队协作的建模方法,通过可视化的方式,让业务和开发一起梳理业务流程,拆分限界上下文。
事件风暴的完整步骤
- 梳理业务场景:先明确我们要梳理的业务范围,比如电商的下单全流程,明确业务目标、参与的角色、核心的业务流程。
- 收集领域事件:领域事件,是业务中已经发生的、对业务有影响的事情,命名格式统一为“XX已XX”,比如“订单已创建”、“订单已支付”、“库存已扣减”、“物流单已创建”。把所有的领域事件,按照业务发生的时间顺序排列。
- 定位触发事件的命令:命令,是触发领域事件的用户操作,命名格式为“XX”,比如“创建订单”触发“订单已创建”,“支付订单”触发“订单已支付”。每个领域事件,都对应一个触发它的命令。
- 找到发起命令的角色:明确每个命令是由谁发起的,比如用户、运营人员、系统定时任务。
- 关联聚合与实体:找到每个命令和事件对应的业务载体,也就是聚合,明确这个业务操作是围绕哪个业务对象进行的,这个业务对象包含哪些属性和规则。
- 划分限界上下文:把语义和业务紧密相关的聚合、事件、命令,放到同一个限界上下文里。划分的核心原则是高内聚、低耦合:同一个上下文内的业务逻辑高度相关,上下文之间的依赖尽量少。
- 确定上下文映射关系:明确不同限界上下文之间的交互方式,也就是上下文映射。
事件风暴落地示例:电商系统
我们以电商系统为例,通过事件风暴拆分出的限界上下文如下:
- 用户域:负责用户的注册、登录、信息管理、等级权益等
- 商品域:负责商品的管理、分类、SKU、价格等
- 订单域:负责订单的创建、支付、发货、完成、取消等全生命周期管理
- 库存域:负责商品库存的扣减、回补、盘点等
- 支付域:负责支付渠道对接、支付流水管理、退款等
- 物流域:负责物流单创建、物流轨迹跟踪、签收管理等
- 营销域:负责优惠活动、优惠券、积分等
- 权限域:负责用户权限、菜单管理、角色管理等
每个限界上下文,都对应一个独立的微服务。这就是基于DDD的微服务拆分,完全按照业务边界拆分,而不是技术边界。
2.4 第四步:上下文映射,处理微服务之间的交互
拆分完限界上下文之后,我们需要明确不同上下文之间的交互关系,也就是上下文映射(Context Mapping)。它决定了微服务之间的调用方式、解耦方案。
常见的上下文映射类型有以下7种,其中最常用的是防腐层、发布-订阅、客户方-供应方:
- 客户方-供应方(Customer-Supplier):两个上下文是上下游关系,客户方上下文依赖供应方上下文的能力,客户方提出需求,供应方提供接口。比如订单域是客户方,商品域是供应方,订单域需要调用商品域的接口查询商品信息。
- 防腐层(Anticorruption Layer,ACL):客户方上下文在自己的域内,定义一套适配接口,把供应方的接口模型转换成自己的领域模型,隔离供应方的变化。即使供应方的接口改了,只需要修改防腐层的适配逻辑,不需要修改自己的核心业务代码。这是跨微服务调用最常用的模式,能最大程度保证自己域的稳定性。
- 发布-订阅(Publish-Subscribe):一个上下文发布领域事件,其他感兴趣的上下文订阅这个事件,做出对应的处理。比如订单域发布“订单已创建”事件,库存域订阅这个事件扣减库存,物流域订阅这个事件创建物流单。这种模式能最大程度解耦微服务,避免同步调用的强依赖。
- 合作关系(Partnership):两个上下文的团队深度合作,共同制定接口规范,一起迭代。比如订单域和营销域,优惠计算和订单创建强相关,两个团队需要一起合作设计。
- 遵奉者(Conformist):客户方完全遵奉供应方的接口模型,不做任何转换,直接使用。这种模式耦合度高,只有在供应方的模型完全符合客户方需求,且非常稳定的情况下使用。
- 共享内核(Shared Kernel):两个上下文共享一部分核心模型和代码,减少重复开发。这种模式耦合度极高,尽量避免使用,除非是两个上下文完全由同一个团队维护。
- 各行其道(Separate Ways):两个上下文没有任何交互,各自独立实现自己的业务,完全解耦。
三、战术设计:限界上下文内的领域模型落地
战略设计确定了微服务的边界,战术设计就是在单个限界上下文内部,完成领域模型的设计和代码落地。战术设计的核心,是充血领域模型,把业务规则和逻辑封装到领域对象里,而不是散落在service层。
3.1 DDD的四层架构
DDD的代码落地,通常采用四层架构,每层有明确的职责,严格遵守依赖倒置原则,保证领域层的核心地位不被技术细节污染。
每层的职责如下:
- 用户接口层(Interfaces):负责和前端/外部系统交互,包含controller、DTO、参数校验、结果统一封装。只负责接收请求和返回结果,不包含任何业务逻辑。
- 应用层(Application):负责业务流程的编排和事务控制,只负责调用领域层的能力,按照业务流程组装步骤,不包含任何核心业务逻辑。比如下单流程:校验用户→创建订单→扣库存→支付→通知物流,应用层只负责编排这个流程,不做任何业务规则判断。
- 领域层(Domain):系统的核心,所有的业务规则、业务逻辑都在这里。包含领域模型、聚合、实体、值对象、领域服务、仓储接口、领域事件。领域层不依赖任何其他层,是完全独立的,不被任何技术细节污染。
- 基础设施层(Infrastructure):负责技术细节的实现,包含数据库、缓存、消息队列、远程调用等技术能力。实现领域层定义的仓储接口,给上层提供技术支撑,依赖领域层,而不是反过来。
3.2 战术设计的核心元素
3.2.1 实体(Entity)
实体是有唯一标识的业务对象,它的核心特征是:
- 有全局唯一的标识,整个生命周期内,标识不变,即使其他属性都变了,只要标识不变,还是同一个实体
- 有自己的生命周期和状态变化
- 包含和自身相关的业务规则和逻辑
比如订单、用户、商品,都是实体,每个订单都有唯一的订单号,不管订单的状态、金额怎么变,订单号不变,还是同一个订单。
3.2.2 值对象(Value Object)
值对象是没有唯一标识的业务对象,它的核心特征是:
- 没有唯一标识,靠属性值来判断是否相等,只要所有属性都一样,就是同一个值对象
- 不可变,一旦创建,就不能修改它的属性,要修改的话,只能创建一个新的值对象替换它
- 只包含数据,不包含生命周期,通常作为实体的属性存在
比如收货地址、订单金额、坐标,都是值对象。收货地址的省、市、区、详细地址都一样,就是同一个地址;如果修改了详细地址,就创建一个新的地址对象,而不是修改原来的对象。
JDK17的record类型,天生就符合值对象的特征,不可变、自动实现equals和hashCode,是值对象的最佳实现方式。
3.2.3 聚合(Aggregate)与聚合根(Aggregate Root)
聚合是一组相关的实体和值对象的集合,是保证数据一致性的最小单元。每个聚合都有一个唯一的入口,就是聚合根。
聚合的核心规则:
- 聚合根是聚合内的一个特殊实体,有全局唯一标识,是外部访问聚合的唯一入口,外部只能通过聚合根来访问聚合内部的对象,不能直接访问聚合内的其他实体。
- 聚合内的实体,只有聚合内的唯一标识,不需要全局唯一标识。
- 一个事务,只能修改一个聚合,保证聚合内的数据强一致性。聚合之间的数据一致性,通过最终一致性来保证。
- 聚合之间不能直接引用,只能通过聚合根的ID来关联。
举个例子,订单聚合:
- 聚合根是Order(订单),有全局唯一的订单号
- 聚合内包含OrderItem(订单项)实体、Address(收货地址)值对象、OrderAmount(订单金额)值对象、OrderStatus(订单状态)枚举
- 外部不能直接修改OrderItem,只能通过Order的addItem、removeItem等方法来操作订单项,保证订单的总金额、商品数量的一致性
- 订单创建、支付、取消等所有业务操作,都必须通过Order聚合根来执行,保证订单的状态流转符合业务规则
常见的错误:把每个数据库表都做成一个聚合根,导致聚合失去了保证数据一致性的作用,最终又回到了贫血模型的老路上。
3.2.4 领域服务(Domain Service)
领域服务是包含核心业务逻辑的服务,它处理的是跨多个聚合的业务逻辑,或者是不适合放在单个聚合/值对象里的业务逻辑。
领域服务的核心规则:
- 领域服务包含的是核心业务逻辑,不是流程编排。
- 只有当业务逻辑不属于任何一个聚合/值对象的时候,才放到领域服务里。
- 领域服务属于领域层,和聚合、实体是平等的。
比如订单金额的计算,需要结合订单项、运费、优惠活动、用户等级,跨了多个聚合,这个逻辑就适合放在订单领域服务里。
3.2.5 应用服务(Application Service)
应用服务属于应用层,只负责业务流程的编排和事务控制,不包含任何核心业务逻辑。它的核心职责是:
- 接收用户接口层的请求,调用领域层的聚合、领域服务,完成业务流程的编排。
- 负责事务的控制,保证流程的原子性。
- 不做任何业务规则的判断,所有业务规则都交给领域层处理。
领域服务和应用服务的核心区别:领域服务做业务决策,包含核心业务逻辑;应用服务做流程编排,不包含业务逻辑。这是DDD战术设计最容易混淆的点,很多人把业务逻辑写在应用服务里,导致领域模型变成了贫血模型。
3.2.6 仓储(Repository)
仓储是用来管理聚合的持久化的,它的核心规则:
- 仓储接口定义在领域层,面向领域模型,只提供聚合的保存、查询、更新、删除方法,不暴露任何数据库相关的细节。
- 仓储的实现类放在基础设施层,依赖ORM框架完成数据库操作,实现领域层的仓储接口。
- 一个聚合对应一个仓储,只有聚合根才有仓储,聚合内的其他实体不能有独立的仓储。
仓储和DAO的核心区别:DAO是面向数据库的,直接操作数据库表;仓储是面向领域的,对外提供聚合对象的持久化能力,封装了数据库操作的细节。领域层只依赖仓储接口,不依赖任何ORM框架,实现了业务和技术的解耦。
3.2.7 领域事件(Domain Event)
领域事件是领域内发生的、对业务有影响的事情,用来实现聚合之间、限界上下文之间的解耦。
领域事件的核心规则:
- 领域事件是已经发生的事情,命名格式为“XX已XX”,包含事件发生的时间、相关的聚合根ID、核心业务数据。
- 领域事件在聚合的业务方法执行完成后发布,保证事件的发布和聚合的修改在同一个事务里。
- 同一个限界上下文内的领域事件,可以用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的战术设计,帮我们把业务逻辑封装到领域模型里,让系统的代码结构和业务逻辑完全对齐,即使需求频繁变更,系统也能保持良好的可维护性和扩展性。