欢迎光临
我们一直在努力

第47篇-DDD战术设计-代码落地

【Kotlin + Spring Boot 4 从零到架构师】第 47 篇:DDD 战术设计 — 代码落地

本系列定位:零基础入门,从 Kotlin 语法一路到 Spring Boot 4 高级架构(DDD + Modulith),适合 Java 开发者转型,也适合纯新手系统学习。


本篇你将学到

  • DDD 分层架构:Domain / Application / Infrastructure / API
  • 聚合根的 Kotlin 实现方式
  • Repository 模式(领域接口 + 基础设施实现)
  • 应用服务(Use Case)的职责

学完本篇,你将能把 DDD 理论落地为可运行的 Kotlin 代码。


一、DDD 分层架构

1.1 四层结构

1.1 四层结构

下面是 DDD 四层架构的 Mermaid 示意图,清晰地展示了各层之间的依赖关系:

#mermaid-svg-YWjCSv82UM2UwaaZ{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-YWjCSv82UM2UwaaZ .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-YWjCSv82UM2UwaaZ .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-YWjCSv82UM2UwaaZ .error-icon{fill:#552222;}#mermaid-svg-YWjCSv82UM2UwaaZ .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-YWjCSv82UM2UwaaZ .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-YWjCSv82UM2UwaaZ .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-YWjCSv82UM2UwaaZ .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-YWjCSv82UM2UwaaZ .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-YWjCSv82UM2UwaaZ .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-YWjCSv82UM2UwaaZ .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-YWjCSv82UM2UwaaZ .marker{fill:#333333;stroke:#333333;}#mermaid-svg-YWjCSv82UM2UwaaZ .marker.cross{stroke:#333333;}#mermaid-svg-YWjCSv82UM2UwaaZ svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-YWjCSv82UM2UwaaZ p{margin:0;}#mermaid-svg-YWjCSv82UM2UwaaZ .label{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;color:#333;}#mermaid-svg-YWjCSv82UM2UwaaZ .cluster-label text{fill:#333;}#mermaid-svg-YWjCSv82UM2UwaaZ .cluster-label span{color:#333;}#mermaid-svg-YWjCSv82UM2UwaaZ .cluster-label span p{background-color:transparent;}#mermaid-svg-YWjCSv82UM2UwaaZ .label text,#mermaid-svg-YWjCSv82UM2UwaaZ span{fill:#333;color:#333;}#mermaid-svg-YWjCSv82UM2UwaaZ .node rect,#mermaid-svg-YWjCSv82UM2UwaaZ .node circle,#mermaid-svg-YWjCSv82UM2UwaaZ .node ellipse,#mermaid-svg-YWjCSv82UM2UwaaZ .node polygon,#mermaid-svg-YWjCSv82UM2UwaaZ .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-YWjCSv82UM2UwaaZ .rough-node .label text,#mermaid-svg-YWjCSv82UM2UwaaZ .node .label text,#mermaid-svg-YWjCSv82UM2UwaaZ .image-shape .label,#mermaid-svg-YWjCSv82UM2UwaaZ .icon-shape .label{text-anchor:middle;}#mermaid-svg-YWjCSv82UM2UwaaZ .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-YWjCSv82UM2UwaaZ .rough-node .label,#mermaid-svg-YWjCSv82UM2UwaaZ .node .label,#mermaid-svg-YWjCSv82UM2UwaaZ .image-shape .label,#mermaid-svg-YWjCSv82UM2UwaaZ .icon-shape .label{text-align:center;}#mermaid-svg-YWjCSv82UM2UwaaZ .node.clickable{cursor:pointer;}#mermaid-svg-YWjCSv82UM2UwaaZ .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-YWjCSv82UM2UwaaZ .arrowheadPath{fill:#333333;}#mermaid-svg-YWjCSv82UM2UwaaZ .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-YWjCSv82UM2UwaaZ .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-YWjCSv82UM2UwaaZ .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YWjCSv82UM2UwaaZ .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-YWjCSv82UM2UwaaZ .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YWjCSv82UM2UwaaZ .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-YWjCSv82UM2UwaaZ .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-YWjCSv82UM2UwaaZ .cluster text{fill:#333;}#mermaid-svg-YWjCSv82UM2UwaaZ .cluster span{color:#333;}#mermaid-svg-YWjCSv82UM2UwaaZ div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-YWjCSv82UM2UwaaZ .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-YWjCSv82UM2UwaaZ rect.text{fill:none;stroke-width:0;}#mermaid-svg-YWjCSv82UM2UwaaZ .icon-shape,#mermaid-svg-YWjCSv82UM2UwaaZ .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-YWjCSv82UM2UwaaZ .icon-shape p,#mermaid-svg-YWjCSv82UM2UwaaZ .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-YWjCSv82UM2UwaaZ .icon-shape .label rect,#mermaid-svg-YWjCSv82UM2UwaaZ .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-YWjCSv82UM2UwaaZ .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-YWjCSv82UM2UwaaZ .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-YWjCSv82UM2UwaaZ :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

实现 Domain 接口

不依赖任何外部层

Infrastructure 层 (基础设施)

Repository 实现、外部 API、消息队列

Domain 层 (领域模型)

聚合根、实体、值对象、领域事件、领域服务

Application 层 (应用服务)

Use Case、事务边界、编排

API 层 (Interface / Presentation)

Controller、DTO、序列化

1.2 依赖方向

API → Application → Domain ← Infrastructure

Infrastructure 实现 Domain 的接口

核心原则:Domain 层不依赖任何外部层——不依赖数据库、不依赖 Web 框架。它是纯粹的 Kotlin 业务代码。

1.3 目录结构

com.example.minishop/
├── order/ ← 订单模块(限界上下文)
│ ├── domain/ ← 领域层
│ │ ├── model/ ← 聚合根、实体、值对象
│ │ │ ├── Order.kt
│ │ │ ├── OrderItem.kt
│ │ │ ├── ShippingAddress.kt
│ │ │ └── OrderStatus.kt
│ │ ├── event/ ← 领域事件
│ │ │ ├── OrderCreatedEvent.kt
│ │ │ └── OrderPaidEvent.kt
│ │ ├── repository/ ← Repository 接口(领域层定义)
│ │ │ └── OrderRepository.kt
│ │ └── service/ ← 领域服务
│ │ └── OrderDomainService.kt
│ ├── application/ ← 应用层
│ │ ├── command/ ← 命令(请求 DTO)
│ │ │ ├── PlaceOrderCommand.kt
│ │ │ └── CancelOrderCommand.kt
│ │ ├── query/ ← 查询(响应 DTO)
│ │ │ └── OrderQueryService.kt
│ │ └── service/ ← 应用服务(Use Case)
│ │ └── OrderApplicationService.kt
│ ├── infrastructure/ ← 基础设施层
│ │ ├── persistence/ ← Repository 实现
│ │ │ ├── OrderEntity.kt ← JPA 实体
│ │ │ ├── OrderJpaRepository.kt
│ │ │ └── OrderRepositoryImpl.kt
│ │ └── messaging/ ← 消息发布
│ │ └── OrderEventPublisher.kt
│ └── api/ ← API 层
│ ├── OrderController.kt
│ └── dto/
│ ├── OrderRequest.kt
│ └── OrderResponse.kt


二、领域层实现

2.1 值对象

package com.example.minishop.order.domain.model

import java.math.BigDecimal

/**
* 金额值对象
*/

data class Money(
val amount: BigDecimal,
val currency: String = "CNY"
) {
init {
require(amount >= BigDecimal.ZERO) { "金额不能为负数" }
}

operator fun times(quantity: Int): Money {
return Money(amount.multiply(BigDecimal(quantity)), currency)
}

operator fun plus(other: Money): Money {
require(currency == other.currency) { "货币不一致" }
return Money(amount + other.amount, currency)
}

companion object {
fun zero() = Money(BigDecimal.ZERO)
}
}

/**
* 收货地址值对象
*/

data class ShippingAddress(
val recipient: String,
val phone: String,
val province: String,
val city: String,
val detail: String
) {
init {
require(recipient.isNotBlank()) { "收件人不能为空" }
require(phone.isNotBlank()) { "手机号不能为空" }
}

fun fullAddress(): String = "$province$city$detail"
}

值对象的 init 块:创建时自动校验业务规则,非法值直接抛异常。这是 DDD 的「不变量保护」。

2.2 枚举(状态)

enum class OrderStatus {
PENDING, // 待支付
PAID, // 已支付
SHIPPED, // 已发货
DELIVERED, // 已送达
CANCELLED; // 已取消

fun canTransitionTo(newStatus: OrderStatus): Boolean {
return when (this) {
PENDING -> newStatus in listOf(PAID, CANCELLED)
PAID -> newStatus in listOf(SHIPPED, CANCELLED)
SHIPPED -> newStatus == DELIVERED
DELIVERED, CANCELLED -> false // 终态不可变更
}
}
}

下面是订单状态机的 Mermaid 状态图,直观展示了 OrderStatus 的合法流转路径:

#mermaid-svg-Xn8mlGlSITv1KZPp{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-Xn8mlGlSITv1KZPp .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-Xn8mlGlSITv1KZPp .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-Xn8mlGlSITv1KZPp .error-icon{fill:#552222;}#mermaid-svg-Xn8mlGlSITv1KZPp .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-Xn8mlGlSITv1KZPp .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-Xn8mlGlSITv1KZPp .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-Xn8mlGlSITv1KZPp .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-Xn8mlGlSITv1KZPp .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-Xn8mlGlSITv1KZPp .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-Xn8mlGlSITv1KZPp .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-Xn8mlGlSITv1KZPp .marker{fill:#333333;stroke:#333333;}#mermaid-svg-Xn8mlGlSITv1KZPp .marker.cross{stroke:#333333;}#mermaid-svg-Xn8mlGlSITv1KZPp svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-Xn8mlGlSITv1KZPp p{margin:0;}#mermaid-svg-Xn8mlGlSITv1KZPp defs #statediagram-barbEnd{fill:#333333;stroke:#333333;}#mermaid-svg-Xn8mlGlSITv1KZPp g.stateGroup text{fill:#9370DB;stroke:none;font-size:10px;}#mermaid-svg-Xn8mlGlSITv1KZPp g.stateGroup text{fill:#333;stroke:none;font-size:10px;}#mermaid-svg-Xn8mlGlSITv1KZPp g.stateGroup .state-title{font-weight:bolder;fill:#131300;}#mermaid-svg-Xn8mlGlSITv1KZPp g.stateGroup rect{fill:#ECECFF;stroke:#9370DB;}#mermaid-svg-Xn8mlGlSITv1KZPp g.stateGroup line{stroke:#333333;stroke-width:1;}#mermaid-svg-Xn8mlGlSITv1KZPp .transition{stroke:#333333;stroke-width:1;fill:none;}#mermaid-svg-Xn8mlGlSITv1KZPp .stateGroup .composit{fill:white;border-bottom:1px;}#mermaid-svg-Xn8mlGlSITv1KZPp .stateGroup .alt-composit{fill:#e0e0e0;border-bottom:1px;}#mermaid-svg-Xn8mlGlSITv1KZPp .state-note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-Xn8mlGlSITv1KZPp .state-note text{fill:black;stroke:none;font-size:10px;}#mermaid-svg-Xn8mlGlSITv1KZPp .stateLabel .box{stroke:none;stroke-width:0;fill:#ECECFF;opacity:0.5;}#mermaid-svg-Xn8mlGlSITv1KZPp .edgeLabel .label rect{fill:#ECECFF;opacity:0.5;}#mermaid-svg-Xn8mlGlSITv1KZPp .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-Xn8mlGlSITv1KZPp .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-Xn8mlGlSITv1KZPp .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-Xn8mlGlSITv1KZPp .edgeLabel .label text{fill:#333;}#mermaid-svg-Xn8mlGlSITv1KZPp .label div .edgeLabel{color:#333;}#mermaid-svg-Xn8mlGlSITv1KZPp .stateLabel text{fill:#131300;font-size:10px;font-weight:bold;}#mermaid-svg-Xn8mlGlSITv1KZPp .node circle.state-start{fill:#333333;stroke:#333333;}#mermaid-svg-Xn8mlGlSITv1KZPp .node .fork-join{fill:#333333;stroke:#333333;}#mermaid-svg-Xn8mlGlSITv1KZPp .node circle.state-end{fill:#9370DB;stroke:white;stroke-width:1.5;}#mermaid-svg-Xn8mlGlSITv1KZPp .end-state-inner{fill:white;stroke-width:1.5;}#mermaid-svg-Xn8mlGlSITv1KZPp .node rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Xn8mlGlSITv1KZPp .node polygon{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Xn8mlGlSITv1KZPp #statediagram-barbEnd{fill:#333333;}#mermaid-svg-Xn8mlGlSITv1KZPp .statediagram-cluster rect{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-Xn8mlGlSITv1KZPp .cluster-label,#mermaid-svg-Xn8mlGlSITv1KZPp .nodeLabel{color:#131300;}#mermaid-svg-Xn8mlGlSITv1KZPp .statediagram-cluster rect.outer{rx:5px;ry:5px;}#mermaid-svg-Xn8mlGlSITv1KZPp .statediagram-state .divider{stroke:#9370DB;}#mermaid-svg-Xn8mlGlSITv1KZPp .statediagram-state .title-state{rx:5px;ry:5px;}#mermaid-svg-Xn8mlGlSITv1KZPp .statediagram-cluster.statediagram-cluster .inner{fill:white;}#mermaid-svg-Xn8mlGlSITv1KZPp .statediagram-cluster.statediagram-cluster-alt .inner{fill:#f0f0f0;}#mermaid-svg-Xn8mlGlSITv1KZPp .statediagram-cluster .inner{rx:0;ry:0;}#mermaid-svg-Xn8mlGlSITv1KZPp .statediagram-state rect.basic{rx:5px;ry:5px;}#mermaid-svg-Xn8mlGlSITv1KZPp .statediagram-state rect.divider{stroke-dasharray:10,10;fill:#f0f0f0;}#mermaid-svg-Xn8mlGlSITv1KZPp .note-edge{stroke-dasharray:5;}#mermaid-svg-Xn8mlGlSITv1KZPp .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-Xn8mlGlSITv1KZPp .statediagram-note rect{fill:#fff5ad;stroke:#aaaa33;stroke-width:1px;rx:0;ry:0;}#mermaid-svg-Xn8mlGlSITv1KZPp .statediagram-note text{fill:black;}#mermaid-svg-Xn8mlGlSITv1KZPp .statediagram-note .nodeLabel{color:black;}#mermaid-svg-Xn8mlGlSITv1KZPp .statediagram .edgeLabel{color:red;}#mermaid-svg-Xn8mlGlSITv1KZPp #dependencyStart,#mermaid-svg-Xn8mlGlSITv1KZPp #dependencyEnd{fill:#333333;stroke:#333333;stroke-width:1;}#mermaid-svg-Xn8mlGlSITv1KZPp .statediagramTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-Xn8mlGlSITv1KZPp :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

创建订单

支付

取消

发货

取消

确认送达

完成

完成

PENDING

PAID

CANCELLED

SHIPPED

DELIVERED

2.3 聚合根——核心

package com.example.minishop.order.domain.model

import java.time.LocalDateTime

/**
* 订单聚合根
* 外部代码只能通过 Order 的方法操作聚合内部数据
*/

class Order(

val id: Long? = null,

val userId: Long,

var status: OrderStatus = OrderStatus.PENDING,

private val _items: MutableList<OrderItem> = mutableListOf(),

var shippingAddress: ShippingAddress? = null,

val createdAt: LocalDateTime = LocalDateTime.now()

) {
// 对外只读视图
val items: List<OrderItem> get() = _items.toList()

// 计算属性——总金额
val totalAmount: Money
get() = _items.fold(Money.zero()) { acc, item -> acc + item.subtotal }

// ==================== 聚合行为 ====================

/**
* 添加订单明细
*/

fun addItem(productId: Long, productName: String, unitPrice: Money, quantity: Int) {
require(status == OrderStatus.PENDING) { "只有待支付订单才能添加商品" }
require(quantity > 0) { "数量必须大于 0" }

_items.add(OrderItem(
orderId = this.id,
productId = productId,
productName = productName,
unitPrice = unitPrice,
quantity = quantity
))
}

/**
* 支付订单
*/

fun pay() {
require(status == OrderStatus.PENDING) { "只有待支付订单才能支付" }
require(_items.isNotEmpty()) { "空订单不能支付" }
status = OrderStatus.PAID
}

/**
* 发货
*/

fun ship() {
require(status == OrderStatus.PAID) { "只有已支付订单才能发货" }
require(shippingAddress != null) { "请先设置收货地址" }
status = OrderStatus.SHIPPED
}

/**
* 取消订单
*/

fun cancel(reason: String) {
require(status in listOf(OrderStatus.PENDING, OrderStatus.PAID)) {
"当前状态($status)不可取消"
}
require(reason.isNotBlank()) { "取消原因不能为空" }
status = OrderStatus.CANCELLED
}

/**
* 修改收货地址(仅待支付状态可修改)
*/

fun changeShippingAddress(address: ShippingAddress) {
require(status == OrderStatus.PENDING) { "只有待支付订单才能修改地址" }
shippingAddress = address
}
}

聚合根的核心原则:

  • 所有修改通过方法进行(不暴露 var _items)
  • 方法内校验业务规则(require)
  • 状态变更受控(canTransitionTo)
  • 外部无法绕过聚合根直接操作内部对象

2.4 实体

/**
* 订单明细实体(属于 Order 聚合)
*/

class OrderItem(
val orderId: Long?,
val productId: Long,
val productName: String,
val unitPrice: Money,
var quantity: Int
) {
val subtotal: Money
get() = unitPrice * quantity

init {
require(quantity > 0) { "数量必须大于 0" }
}
}

2.5 Repository 接口(领域层定义)

package com.example.minishop.order.domain.repository

import com.example.minishop.order.domain.model.Order

/**
* 订单 Repository 接口(领域层定义,基础设施层实现)
*/

interface OrderRepository {
fun save(order: Order): Order
fun findById(id: Long): Order?
fun findByUserId(userId: Long): List<Order>
}

关键:Repository 接口定义在领域层(用领域模型 Order),实现类在基础设施层(把 Order 转换为 JPA 实体)。领域层不依赖 JPA。


三、应用层

3.1 命令对象

package com.example.minishop.order.application.command

data class PlaceOrderCommand(
val userId: Long,
val items: List<OrderItemCommand>
)

data class OrderItemCommand(
val productId: Long,
val quantity: Int
)

data class CancelOrderCommand(
val orderId: Long,
val userId: Long,
val reason: String
)

3.2 应用服务

package com.example.minishop.order.application.service

@Service
class OrderApplicationService(
private val orderRepository: OrderRepository,
private val productRepository: ProductRepository, // 领域接口
private val eventPublisher: DomainEventPublisher
) {
/**
* 下单
*/

@Transactional
fun placeOrder(command: PlaceOrderCommand): Long {
// 1. 查商品信息
val productInfos = command.items.map { item ->
val product = productRepository.findById(item.productId)
?: throw ResourceNotFoundException("商品", item.productId)
ProductOrderInfo(
productId = product.id!!,
productName = product.name,
unitPrice = Money(product.price),
quantity = item.quantity
)
}

// 2. 创建聚合根
val order = Order(userId = command.userId)
productInfos.forEach { info ->
order.addItem(
productId = info.productId,
productName = info.productName,
unitPrice = info.unitPrice,
quantity = info.quantity
)
}

// 3. 保存
val saved = orderRepository.save(order)

// 4. 发布领域事件
eventPublisher.publish(OrderCreatedEvent(
orderId = saved.id!!,
userId = saved.userId,
totalAmount = saved.totalAmount.amount
))

return saved.id!!
}

/**
* 取消订单
*/

@Transactional
fun cancelOrder(command: CancelOrderCommand) {
val order = orderRepository.findById(command.orderId)
?: throw ResourceNotFoundException("订单", command.orderId)

// 数据级权限检查
if (order.userId != command.userId) {
throw AuthorizationException("无权操作此订单")
}

// 聚合根内部处理取消逻辑
order.cancel(command.reason)

orderRepository.save(order)
}

private data class ProductOrderInfo(
val productId: Long,
val productName: String,
val unitPrice: Money,
val quantity: Int
)
}

下面是下单流程的 Mermaid 时序图,展示了应用服务如何编排领域对象、Repository 和事件发布器:

DomainEventPublisher

OrderRepository

Order (聚合根)

ProductRepository

OrderApplicationService

Controller

DomainEventPublisher

OrderRepository

Order (聚合根)

ProductRepository

OrderApplicationService

Controller

#mermaid-svg-g0I7vYtsokdHetXf{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-g0I7vYtsokdHetXf .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-g0I7vYtsokdHetXf .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-g0I7vYtsokdHetXf .error-icon{fill:#552222;}#mermaid-svg-g0I7vYtsokdHetXf .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-g0I7vYtsokdHetXf .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-g0I7vYtsokdHetXf .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-g0I7vYtsokdHetXf .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-g0I7vYtsokdHetXf .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-g0I7vYtsokdHetXf .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-g0I7vYtsokdHetXf .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-g0I7vYtsokdHetXf .marker{fill:#333333;stroke:#333333;}#mermaid-svg-g0I7vYtsokdHetXf .marker.cross{stroke:#333333;}#mermaid-svg-g0I7vYtsokdHetXf svg{font-family:\”trebuchet ms\”,verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-g0I7vYtsokdHetXf p{margin:0;}#mermaid-svg-g0I7vYtsokdHetXf .actor{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-g0I7vYtsokdHetXf text.actor>tspan{fill:black;stroke:none;}#mermaid-svg-g0I7vYtsokdHetXf .actor-line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-g0I7vYtsokdHetXf .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-g0I7vYtsokdHetXf .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-g0I7vYtsokdHetXf .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-g0I7vYtsokdHetXf #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-g0I7vYtsokdHetXf .sequenceNumber{fill:white;}#mermaid-svg-g0I7vYtsokdHetXf #sequencenumber{fill:#333;}#mermaid-svg-g0I7vYtsokdHetXf #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-g0I7vYtsokdHetXf .messageText{fill:#333;stroke:none;}#mermaid-svg-g0I7vYtsokdHetXf .labelBox{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-g0I7vYtsokdHetXf .labelText,#mermaid-svg-g0I7vYtsokdHetXf .labelText>tspan{fill:black;stroke:none;}#mermaid-svg-g0I7vYtsokdHetXf .loopText,#mermaid-svg-g0I7vYtsokdHetXf .loopText>tspan{fill:black;stroke:none;}#mermaid-svg-g0I7vYtsokdHetXf .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);}#mermaid-svg-g0I7vYtsokdHetXf .note{stroke:#aaaa33;fill:#fff5ad;}#mermaid-svg-g0I7vYtsokdHetXf .noteText,#mermaid-svg-g0I7vYtsokdHetXf .noteText>tspan{fill:black;stroke:none;}#mermaid-svg-g0I7vYtsokdHetXf .activation0{fill:#f4f4f4;stroke:#666;}#mermaid-svg-g0I7vYtsokdHetXf .activation1{fill:#f4f4f4;stroke:#666;}#mermaid-svg-g0I7vYtsokdHetXf .activation2{fill:#f4f4f4;stroke:#666;}#mermaid-svg-g0I7vYtsokdHetXf .actorPopupMenu{position:absolute;}#mermaid-svg-g0I7vYtsokdHetXf .actorPopupMenuPanel{position:absolute;fill:#ECECFF;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-g0I7vYtsokdHetXf .actor-man line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;}#mermaid-svg-g0I7vYtsokdHetXf .actor-man circle,#mermaid-svg-g0I7vYtsokdHetXf line{stroke:hsl(259.6261682243, 59.7765363128%, 87.9019607843%);fill:#ECECFF;stroke-width:2px;}#mermaid-svg-g0I7vYtsokdHetXf :root{–mermaid-font-family:\”trebuchet ms\”,verdana,arial,sans-serif;}

loop

[遍历商品]

placeOrder(command)

findById(productId)

Product

new Order(userId)

addItem(productId, …)

save(order)

savedOrder

publish(OrderCreatedEvent)

event published

orderId

3.3 应用服务的职责

✅ 做:事务管理、编排领域对象、权限检查、发布事件
❌ 不做:业务规则判断(这是聚合根的职责)


四、基础设施层

4.1 Repository 实现

package com.example.minishop.order.infrastructure.persistence

import com.example.minishop.order.domain.model.Order
import com.example.minishop.order.domain.repository.OrderRepository
import org.springframework.stereotype.Repository

@Repository
class OrderRepositoryImpl(
private val orderJpaRepository: OrderJpaRepository
) : OrderRepository {

override fun save(order: Order): Order {
val entity = order.toEntity()
val saved = orderJpaRepository.save(entity)
return saved.toDomain()
}

override fun findById(id: Long): Order? {
return orderJpaRepository.findById(id)
.orElse(null)
?.toDomain()
}

override fun findByUserId(userId: Long): List<Order> {
return orderJpaRepository.findByUserId(userId)
.map { it.toDomain() }
}
}

Repository 实现做了两件事:

  • 领域模型 ↔ JPA 实体的双向转换(toEntity() / toDomain())
  • 委托给 Spring Data JPA 的 OrderJpaRepository

  • 五、API 层

    package com.example.minishop.order.api

    @RestController
    @RequestMapping("/api/orders")
    class OrderController(
    private val orderApplicationService: OrderApplicationService
    ) {
    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    fun placeOrder(@RequestBody request: PlaceOrderRequest): ApiResponse<Map<String, Long>> {
    val orderId = orderApplicationService.placeOrder(request.toCommand())
    return ApiResponse.success(mapOf("orderId" to orderId))
    }

    @DeleteMapping("/{id}")
    fun cancelOrder(
    @PathVariable id: Long,
    @RequestBody request: CancelOrderRequest,
    @RequestHeader("X-User-Id") userId: Long
    ): ApiResponse<Unit> {
    orderApplicationService.cancelOrder(CancelOrderCommand(
    orderId = id,
    userId = userId,
    reason = request.reason
    ))
    return ApiResponse.success()
    }
    }


    本篇小结

    知识点核心内容
    四层架构 API → Application → Domain ← Infrastructure
    依赖方向 Domain 不依赖任何外部层
    值对象 data class + init 校验不变量
    聚合根 所有修改通过方法,内部校验业务规则
    枚举 状态机:canTransitionTo 控制合法转换
    Repository 领域层定义接口,基础设施层实现
    应用服务 编排聚合根、事务、权限、事件
    领域模型 ↔ JPA 实体 toDomain() / toEntity() 双向转换

    下篇预告

    第 48 篇:Spring Modulith 模块化架构

    DDD 的限界上下文怎么在代码中强制隔离?下一篇用 Spring Modulith 实现模块边界验证。


    如果本篇内容对你有帮助,欢迎点赞收藏!有任何疑问,欢迎在评论区交流。

    赞(0)
    未经允许不得转载:171主机测评 » 第47篇-DDD战术设计-代码落地
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址