Skip to main content

领域驱动设计架构详解

项目概述

这是一个基于**领域驱动设计(Domain-Driven Design, DDD)**理念构建的完整商品管理系统示例。项目采用Spring Boot 3.0.2 + Java 17技术栈,严格遵循DDD的分层架构和核心设计原则。

项目结构

product-domain/
├── src/main/java/com/example/product/
│ ├── interfaces/ # 接口适配层
│ │ ├── controller/ # REST API控制器
│ │ │ └── ProductController.java
│ │ ├── dto/ # 数据传输对象
│ │ │ ├── ProductCreateRequest.java
│ │ │ ├── ProductResponse.java
│ │ │ └── ProductQueryRequest.java
│ │ └── assembler/ # 对象转换器
│ │ └── ProductAssembler.java
│ │
│ ├── application/ # 应用层
│ │ ├── service/ # 应用服务
│ │ │ └── ProductApplicationService.java
│ │ ├── command/ # 命令对象
│ │ │ └── ProductCommand.java
│ │ └── query/ # 查询对象
│ │ └── ProductQuery.java
│ │
│ ├── domain/ # 领域层(核心)
│ │ ├── product/ # 商品聚合
│ │ │ ├── entity/ # 实体
│ │ │ │ └── Product.java
│ │ │ ├── valueobject/ # 值对象
│ │ │ │ ├── ProductId.java
│ │ │ │ ├── ProductName.java
│ │ │ │ ├── Price.java
│ │ │ │ └── Stock.java
│ │ │ ├── repository/ # 仓储接口
│ │ │ │ └── ProductRepository.java
│ │ │ └── event/ # 领域事件
│ │ │ └── ProductPublishedEvent.java
│ │ ├── category/ # 分类聚合
│ │ └── common/ # 公共领域组件
│ │ ├── DomainEvent.java
│ │ └── BaseEntity.java
│ │
│ ├── infrastructure/ # 基础设施层
│ │ ├── persistence/ # 持久化实现
│ │ │ ├── mapper/ # MyBatis Mapper
│ │ │ │ └── ProductMapper.java
│ │ │ ├── repository/ # 仓储实现
│ │ │ │ └── ProductRepositoryImpl.java
│ │ │ └── do/ # 数据对象
│ │ │ └── ProductDO.java
│ │ └── config/ # 配置文件
│ │
│ └── common/ # 通用模块
│ ├── exception/ # 异常定义
│ ├── result/ # 返回结果
│ └── constants/ # 常量定义

├── src/main/resources/
│ ├── mapper/ # MyBatis XML
│ │ └── ProductMapper.xml
│ └── application.yml

└── pom.xml

架构设计

分层架构

┌─────────────────────────────────────────────────────────────┐
│ 接口适配层 (Interfaces) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ REST API │ │ DTO │ │ InterfaceAssembler │ │
│ │ Controller │ │ │ │ │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ 应用层 (Application) │
│ ┌─────────────────────────────────────────────────────────┐│
│ │ ProductApplicationService ││
│ │ - 用例编排 - 事务管理 - 流程控制 ││
│ └─────────────────────────────────────────────────────────┘│
├─────────────────────────────────────────────────────────────┤
│ 领域层 (Domain) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Product │ │ ValueObjects│ │ ProductRepository │ │
│ │ (聚合根) │ │ (值对象) │ │ (仓储接口) │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
│ ┌─────────────────────────────────────────────────────────┐ │
│ │ ProductDomainService │ │
│ │ - 跨聚合业务逻辑 - 复杂业务规则 │ │
│ └─────────────────────────────────────────────────────────┘ │
├─────────────────────────────────────────────────────────────┤
│ 基础设施层 (Infrastructure) │
│ ┌─────────────┐ ┌─────────────┐ ┌─────────────────────┐ │
│ │ Repository │ │ DataMapper │ │ Assembler │ │
│ │ Implementation│ │ (DO) │ │ (数据转换) │ │
│ └─────────────┘ └─────────────┘ └─────────────────────┘ │
└─────────────────────────────────────────────────────────────┘

核心组件详解

1. 领域层 (Domain Layer)

聚合根 - Product

@Entity
@Table(name = "t_product")
public class Product extends BaseEntity<ProductId> {

// 聚合根标识
@Embedded
private ProductId id;

// 核心业务属性
@Embedded
private ProductName name; // 商品名称

@Embedded
private CategoryId categoryId; // 分类ID

@Embedded
private Price price; // 价格

@Embedded
private Stock stock; // 库存

// 状态管理
@Enumerated(EnumType.ORDINAL)
private PublishStatus publishStatus = PublishStatus.DRAFT; // 发布状态

@Enumerated(EnumType.ORDINAL)
private NewStatus newStatus = NewStatus.NO; // 新品状态

@Enumerated(EnumType.ORDINAL)
private RecommendStatus recommendStatus = RecommendStatus.NO; // 推荐状态

// 私有构造函数(通过工厂方法创建)
private Product() {}

// 工厂方法
public static Product create(ProductName name, CategoryId categoryId,
Price price, Stock stock) {
Product product = new Product();
product.id = new ProductId(IdGenerator.generate());
product.name = name;
product.categoryId = categoryId;
product.price = price;
product.stock = stock;
product.publishStatus = PublishStatus.DRAFT;
product.newStatus = NewStatus.NO;
product.recommendStatus = RecommendStatus.NO;
// 初始化审计字段
product.createdAt = LocalDateTime.now();
product.updatedAt = LocalDateTime.now();
return product;
}

// 业务行为
public void publish() {
if (!canBePublished()) {
throw new DomainException("商品不满足发布条件");
}
this.publishStatus = PublishStatus.PUBLISHED;
this.updatedAt = LocalDateTime.now();
// 发布时设置新品状态
if (this.newStatus == NewStatus.NO) {
this.newStatus = NewStatus.YES;
}
}

public void unpublish() {
this.publishStatus = PublishStatus.UNPUBLISHED;
this.updatedAt = LocalDateTime.now();
}

public void markAsNew() {
this.newStatus = NewStatus.YES;
this.updatedAt = LocalDateTime.now();
}

public void cancelNew() {
this.newStatus = NewStatus.NO;
this.updatedAt = LocalDateTime.now();
}

public boolean canBePublished() {
// 业务规则:商品名称、价格、库存都必须有值才能发布
return name != null && price != null &&
stock != null && stock.getQuantity() > 0;
}

// 修改价格(带业务校验)
public void updatePrice(Price newPrice) {
if (newPrice == null) {
throw new IllegalArgumentException("价格不能为空");
}
this.price = newPrice;
this.updatedAt = LocalDateTime.now();
}

// 扣减库存(带业务校验)
public boolean deductStock(int quantity) {
if (quantity <= 0) {
throw new IllegalArgumentException("扣减数量必须大于0");
}
if (!stock.canDeduct(quantity)) {
return false;
}
stock = stock.deduct(quantity);
this.updatedAt = LocalDateTime.now();
return true;
}

// Getters
public ProductId getId() { return id; }
public ProductName getName() { return name; }
public CategoryId getCategoryId() { return categoryId; }
public Price getPrice() { return price; }
public Stock getStock() { return stock; }
public PublishStatus getPublishStatus() { return publishStatus; }
public NewStatus getNewStatus() { return newStatus; }
public RecommendStatus getRecommendStatus() { return recommendStatus; }
}

值对象 (Value Objects)

值对象封装了业务概念,具有不可变性和值相等性:

  • ProductId - 商品唯一标识
  • ProductName - 商品名称(带验证规则)
  • Price - 价格(带业务约束)
  • Stock - 库存(带库存管理逻辑)
  • CategoryId - 分类标识
  • 各种状态枚举值对象
ProductId 值对象
@Embeddable
@Getter
@EqualsAndHashCode
public class ProductId implements Identifier {
private final String value;

private ProductId(String value) {
if (StringUtils.isBlank(value)) {
throw new IllegalArgumentException("商品ID不能为空");
}
this.value = value;
}

public static ProductId of(String value) {
return new ProductId(value);
}

public static ProductId generate() {
return new ProductId(IdGenerator.generate());
}

@Override
public String getValue() {
return value;
}
}
ProductName 值对象
@Embeddable
@Getter
@EqualsAndHashCode
public class ProductName {
private static final int MAX_LENGTH = 200;
private static final int MIN_LENGTH = 1;

private final String value;

private ProductName(String value) {
if (StringUtils.isBlank(value)) {
throw new IllegalArgumentException("商品名称不能为空");
}
String trimmed = value.trim();
if (trimmed.length() < MIN_LENGTH || trimmed.length() > MAX_LENGTH) {
throw new IllegalArgumentException(
"商品名称长度必须在" + MIN_LENGTH + "到" + MAX_LENGTH + "之间");
}
this.value = trimmed;
}

public static ProductName of(String value) {
return new ProductName(value);
}

public String getValue() { return value; }
}
Price 值对象
@Embeddable
@Getter
@EqualsAndHashCode
public class Price {
private static final BigDecimal MAX_PRICE = new BigDecimal("999999.99");
private static final int SCALE = 2;

private final BigDecimal value;

private Price(BigDecimal value) {
if (value == null) {
throw new IllegalArgumentException("价格不能为空");
}
if (value.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("价格不能为负数");
}
if (value.compareTo(MAX_PRICE) > 0) {
throw new IllegalArgumentException("价格不能超过" + MAX_PRICE);
}
this.value = value.setScale(SCALE, BigDecimal.ROUND_HALF_UP);
}

public static Price of(BigDecimal value) {
return new Price(value);
}

public static Price zero() {
return new Price(BigDecimal.ZERO);
}

// 业务行为
public Price add(Price other) {
return new Price(this.value.add(other.value));
}

public Price multiply(int quantity) {
return new Price(this.value.multiply(BigDecimal.valueOf(quantity)));
}

public boolean isGreaterThan(Price other) {
return this.value.compareTo(other.value) > 0;
}

public BigDecimal getValue() { return value; }
}
Stock 值对象
@Embeddable
@Getter
@EqualsAndHashCode
public class Stock {
private final int quantity; // 当前库存
private final int reserved; // 预留库存
private final int warningThreshold; // 预警阈值

private Stock(int quantity, int reserved, int warningThreshold) {
if (quantity < 0) {
throw new IllegalArgumentException("库存不能为负数");
}
if (reserved < 0) {
throw new IllegalArgumentException("预留库存不能为负数");
}
this.quantity = quantity;
this.reserved = reserved;
this.warningThreshold = warningThreshold;
}

public static Stock create(int quantity) {
return new Stock(quantity, 0, 10);
}

public static Stock create(int quantity, int warningThreshold) {
return new Stock(quantity, 0, warningThreshold);
}

// 可用库存
public int getAvailableQuantity() {
return quantity - reserved;
}

// 是否需要预警
public boolean isWarning() {
return quantity <= warningThreshold;
}

// 是否可以扣减
public boolean canDeduct(int quantity) {
return getAvailableQuantity() >= quantity;
}

// 扣减库存
public Stock deduct(int quantity) {
if (!canDeduct(quantity)) {
throw new DomainException("库存不足,当前可用:" + getAvailableQuantity());
}
return new Stock(this.quantity - quantity, this.reserved, this.warningThreshold);
}

// 增加库存
public Stock increase(int quantity) {
if (quantity <= 0) {
throw new IllegalArgumentException("增加数量必须大于0");
}
return new Stock(this.quantity + quantity, this.reserved, this.warningThreshold);
}
}
状态枚举
public enum PublishStatus {
DRAFT(0, "草稿"),
PUBLISHED(1, "已发布"),
UNPUBLISHED(2, "已下架");

private final int code;
private final String desc;

PublishStatus(int code, String desc) {
this.code = code;
this.desc = desc;
}
}

public enum NewStatus {
NO(0, "非新品"),
YES(1, "新品");

private final int code;
private final String desc;
// ...
}

public enum RecommendStatus {
NO(0, "非推荐"),
YES(1, "推荐");

private final int code;
private final String desc;
// ...
}

基础实体类

@MappedSuperclass
public abstract class BaseEntity<ID extends Identifier> {

@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long dbId;

// 审计字段
protected LocalDateTime createdAt;
protected LocalDateTime updatedAt;
protected Long creatorId;
protected Long updaterId;

// 逻辑删除
@Column(name = "is_deleted")
@ColumnDefault("0")
private Boolean isDeleted = false;

public abstract ID getId();

public LocalDateTime getCreatedAt() { return createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public Boolean getIsDeleted() { return isDeleted; }

public void markDeleted() {
this.isDeleted = true;
this.updatedAt = LocalDateTime.now();
}

public void markUndeleted() {
this.isDeleted = false;
this.updatedAt = LocalDateTime.now();
}
}

领域事件

public abstract class DomainEvent {
private final LocalDateTime occurredOn;
private final String eventId;

protected DomainEvent() {
this.occurredOn = LocalDateTime.now();
this.eventId = UUID.randomUUID().toString();
}

public LocalDateTime getOccurredOn() { return occurredOn; }
public String getEventId() { return eventId; }
}

// 商品发布事件
public class ProductPublishedEvent extends DomainEvent {
private final ProductId productId;
private final ProductName productName;

public ProductPublishedEvent(ProductId productId, ProductName productName) {
super();
this.productId = productId;
this.productName = productName;
}

public ProductId getProductId() { return productId; }
public ProductName getProductName() { return productName; }
}

仓储接口

public interface ProductRepository {
// 保存(新增或更新)
void save(Product product);

// 根据ID查询
Optional<Product> findById(ProductId productId);

// 根据分类查询
List<Product> findByCategoryId(CategoryId categoryId);

// 根据发布状态查询
List<Product> findByPublishStatus(PublishStatus publishStatus);

// 根据名称模糊查询
List<Product> findByNameLike(String nameKeyword);

// 根据ID列表批量查询
List<Product> findByIds(List<ProductId> ids);

// 统计分类下商品数量
long countByCategoryId(CategoryId categoryId);

// 删除
void delete(ProductId productId);
}

领域服务

@Service
@RequiredArgsConstructor
public class ProductDomainService {

private final ProductRepository productRepository;

/**
* 创建商品
* 封装复杂的创建逻辑
*/
public Product createProduct(ProductName name, CategoryId categoryId,
Price price, Stock stock) {
// 业务规则验证
validateProductCreation(name, categoryId, price, stock);

// 创建聚合根
Product product = Product.create(name, categoryId, price, stock);

return product;
}

/**
* 发布商品(跨聚合业务逻辑)
*/
public void publishProduct(Product product) {
// 业务规则验证
if (!product.canBePublished()) {
throw new DomainException("商品不满足发布条件");
}

// 发布商品
product.publish();

// 保存
productRepository.save(product);
}

/**
* 修改价格(带业务规则)
*/
public void updatePrice(Product product, Price newPrice) {
// 业务规则:涨幅不能超过原价的50%
Price oldPrice = product.getPrice();
BigDecimal increaseRate = newPrice.getValue()
.subtract(oldPrice.getValue())
.divide(oldPrice.getValue(), 4, BigDecimal.ROUND_HALF_UP);

if (increaseRate.compareTo(new BigDecimal("0.5")) > 0) {
throw new DomainException("价格涨幅不能超过50%");
}

product.updatePrice(newPrice);
productRepository.save(product);
}

/**
* 批量设置新品状态
*/
public void batchMarkAsNew(List<ProductId> productIds) {
List<Product> products = productRepository.findByIds(productIds);

for (Product product : products) {
if (product.getNewStatus() == NewStatus.NO) {
product.markAsNew();
productRepository.save(product);
}
}
}

/**
* 校验商品创建
*/
private void validateProductCreation(ProductName name, CategoryId categoryId,
Price price, Stock stock) {
if (name == null || StringUtils.isBlank(name.getValue())) {
throw new IllegalArgumentException("商品名称不能为空");
}
if (price == null || price.getValue().compareTo(BigDecimal.ZERO) <= 0) {
throw new IllegalArgumentException("价格必须大于0");
}
if (stock == null || stock.getQuantity() < 0) {
throw new IllegalArgumentException("库存不能为负数");
}
}
}

领域事件处理器

@Component
@RequiredArgsConstructor
public class ProductEventHandler {

private final NotificationService notificationService;
private final CacheService cacheService;

/**
* 处理商品发布事件
*/
@Async
@EventListener
public void handleProductPublished(ProductPublishedEvent event) {
// 发送通知
notificationService.notifyProductPublished(
event.getProductId(),
event.getProductName()
);

// 清理缓存
cacheService.evictProductListCache();
}
}

2. 应用层 (Application Layer)

应用服务负责协调领域对象和基础设施:

@Service
@RequiredArgsConstructor
public class ProductApplicationService {

private final ProductRepository productRepository;
private final ProductDomainService productDomainService;
private final ApplicationEventPublisher eventPublisher;

/**
* 创建商品
*/
@Transactional(rollbackFor = Exception.class)
public ProductId createProduct(ProductCreateCommand command) {
// 1. 构建值对象
ProductName name = ProductName.of(command.getName());
CategoryId categoryId = CategoryId.of(command.getCategoryId());
Price price = Price.of(new BigDecimal(command.getPrice()));
Stock stock = Stock.create(command.getStock());

// 2. 调用领域服务创建商品
Product product = productDomainService.createProduct(name, categoryId, price, stock);

// 3. 持久化
productRepository.save(product);

// 4. 返回结果
return product.getId();
}

/**
* 发布商品
*/
@Transactional(rollbackFor = Exception.class)
public void publishProduct(ProductId productId) {
// 1. 查询聚合根
Product product = productRepository.findById(productId)
.orElseThrow(() -> new DomainException("商品不存在"));

// 2. 调用聚合根的业务方法
product.publish();

// 3. 持久化
productRepository.save(product);

// 4. 发布领域事件
eventPublisher.publishEvent(
new ProductPublishedEvent(product.getId(), product.getName())
);
}

/**
* 扣减库存
*/
@Transactional(rollbackFor = Exception.class)
public boolean deductStock(ProductId productId, int quantity) {
Product product = productRepository.findById(productId)
.orElseThrow(() -> new DomainException("商品不存在"));

boolean success = product.deductStock(quantity);
if (success) {
productRepository.save(product);
}
return success;
}

/**
* 查询已发布商品列表
*/
public List<Product> findPublishedProducts() {
return productRepository.findByPublishStatus(PublishStatus.PUBLISHED);
}

/**
* 分页查询商品
*/
public Page<Product> findProducts(ProductQuery query) {
return productRepository.findByPage(query);
}
}

命令对象

@Data
public class ProductCreateCommand {
@NotBlank(message = "商品名称不能为空")
private String name;

@NotNull(message = "分类ID不能为空")
private Long categoryId;

@NotBlank(message = "价格不能为空")
@DecimalMin(value = "0.01", message = "价格必须大于0")
private String price;

@NotNull(message = "库存不能为空")
@Min(value = 0, message = "库存不能为负数")
private Integer stock;

private String description;
private List<String> images;
}

@Data
public class ProductUpdateCommand {
private ProductId productId;

private String name;
private String price;
private Integer stock;
private String description;
}

3. 基础设施层 (Infrastructure Layer)

数据对象 (DO)

@TableName("t_product")
@Data
public class ProductDO {
private String id; // 商品ID
private String name; // 商品名称
private Long categoryId; // 分类ID
private BigDecimal price; // 价格
private Integer stock; // 库存
private Integer reserved; // 预留库存
private Integer warningThreshold; // 预警阈值
private Integer publishStatus; // 发布状态
private Integer newStatus; // 新品状态
private Integer recommendStatus; // 推荐状态
private String description; // 描述
private String images; // 图片JSON

// 审计字段
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
private Long creatorId;
private Long updaterId;
private Boolean isDeleted;
}

仓储实现

@Repository
@RequiredArgsConstructor
public class ProductRepositoryImpl implements ProductRepository {

private final ProductMapper productMapper;

@Override
public void save(Product product) {
ProductDO productDO = ProductAssembler.toDO(product);

// 判断是新增还是更新
if (productMapper.selectById(product.getId().getValue()) == null) {
productMapper.insert(productDO);
} else {
productMapper.updateById(productDO);
}
}

@Override
public Optional<Product> findById(ProductId productId) {
ProductDO productDO = productMapper.selectById(productId.getValue());
if (productDO == null || productDO.getIsDeleted()) {
return Optional.empty();
}
return Optional.of(ProductAssembler.toDomain(productDO));
}

@Override
public List<Product> findByCategoryId(CategoryId categoryId) {
List<ProductDO> dos = productMapper.selectByCategoryId(categoryId.getValue());
return dos.stream()
.filter(d -> !d.getIsDeleted())
.map(ProductAssembler::toDomain)
.collect(Collectors.toList());
}

@Override
public List<Product> findByPublishStatus(PublishStatus publishStatus) {
List<ProductDO> dos = productMapper.selectByPublishStatus(publishStatus.getCode());
return dos.stream()
.map(ProductAssembler::toDomain)
.collect(Collectors.toList());
}

@Override
public List<Product> findByIds(List<ProductId> ids) {
List<String> idValues = ids.stream()
.map(Identifier::getValue)
.collect(Collectors.toList());
List<ProductDO> dos = productMapper.selectByIds(idValues);
return dos.stream()
.filter(d -> !d.getIsDeleted())
.map(ProductAssembler::toDomain)
.collect(Collectors.toList());
}

@Override
public void delete(ProductId productId) {
productMapper.deleteById(productId.getValue());
}
}

数据映射与装配

@Component
public class ProductAssembler {

/**
* 领域对象 -> 数据对象
*/
public static ProductDO toDO(Product product) {
if (product == null) {
return null;
}

ProductDO productDO = new ProductDO();
productDO.setId(product.getId().getValue());
productDO.setName(product.getName().getValue());
productDO.setCategoryId(product.getCategoryId().getValue());
productDO.setPrice(product.getPrice().getValue());
productDO.setStock(product.getStock().getQuantity());
productDO.setReserved(product.getStock().getReserved());
productDO.setWarningThreshold(product.getStock().getWarningThreshold());
productDO.setPublishStatus(product.getPublishStatus().getCode());
productDO.setNewStatus(product.getNewStatus().getCode());
productDO.setRecommendStatus(product.getRecommendStatus().getCode());

// 审计字段
productDO.setCreatedAt(product.getCreatedAt());
productDO.setUpdatedAt(product.getUpdatedAt());

return productDO;
}

/**
* 数据对象 -> 领域对象
*/
public static Product toDomain(ProductDO productDO) {
if (productDO == null) {
return null;
}

Product product = new Product();
// 反射或直接设置(实际项目推荐用Builder或构造函数)
// 这里简化处理
return product;
}
}

MyBatis Mapper

@Mapper
public interface ProductMapper {

@Select("SELECT * FROM t_product WHERE id = #{id} AND is_deleted = 0")
ProductDO selectById(String id);

@Select("SELECT * FROM t_product WHERE category_id = #{categoryId} AND is_deleted = 0")
List<ProductDO> selectByCategoryId(Long categoryId);

@Select("SELECT * FROM t_product WHERE publish_status = #{status} AND is_deleted = 0")
List<ProductDO> selectByPublishStatus(Integer status);

@Select("<script>" +
"SELECT * FROM t_product WHERE is_deleted = 0 " +
"AND id IN " +
"<foreach collection='ids' item='id' open='(' separator=',' close=')'>" +
"#{id}" +
"</foreach>" +
"</script>")
List<ProductDO> selectByIds(List<String> ids);

@Insert("INSERT INTO t_product (id, name, category_id, price, stock, publish_status, ...) " +
"VALUES (#{id}, #{name}, #{categoryId}, #{price}, #{stock}, #{publishStatus}, ...)")
void insert(ProductDO productDO);

@Update("UPDATE t_product SET name=#{name}, price=#{price}, stock=#{stock}, ... WHERE id=#{id}")
void updateById(ProductDO productDO);

@Update("UPDATE t_product SET is_deleted = 1 WHERE id = #{id}")
void deleteById(String id);
}

4. 接口层 (Interface Layer)

REST控制器

@RestController
@RequestMapping("/api/products")
@RequiredArgsConstructor
public class ProductController {

private final ProductApplicationService productApplicationService;

/**
* 创建商品
*/
@PostMapping
public Result<ProductId> createProduct(@Valid @RequestBody ProductCreateRequest request) {
ProductCreateCommand command = ProductAssembler.toCommand(request);
ProductId productId = productApplicationService.createProduct(command);
return Result.success(productId);
}

/**
* 发布商品
*/
@PostMapping("/{id}/publish")
public Result<Void> publishProduct(@PathVariable("id") String productId) {
ProductId id = ProductId.of(productId);
productApplicationService.publishProduct(id);
return Result.success();
}

/**
* 下架商品
*/
@PostMapping("/{id}/unpublish")
public Result<Void> unpublishProduct(@PathVariable("id") String productId) {
ProductId id = ProductId.of(productId);
productApplicationService.unpublishProduct(id);
return Result.success();
}

/**
* 获取商品详情
*/
@GetMapping("/{id}")
public Result<ProductResponse> getProduct(@PathVariable("id") String productId) {
ProductId id = ProductId.of(productId);
Optional<Product> productOpt = productApplicationService.findById(id);
if (!productOpt.isPresent()) {
return Result.error("商品不存在");
}
ProductResponse response = ProductAssembler.toResponse(productOpt.get());
return Result.success(response);
}

/**
* 查询商品列表
*/
@GetMapping
public Result<Page<ProductResponse>> listProducts(
@RequestParam(required = false) String name,
@RequestParam(required = false) Long categoryId,
@RequestParam(defaultValue = "1") Integer pageNum,
@RequestParam(defaultValue = "10") Integer pageSize) {
ProductQuery query = ProductQuery.builder()
.name(name)
.categoryId(categoryId)
.pageNum(pageNum)
.pageSize(pageSize)
.build();

Page<Product> page = productApplicationService.findProducts(query);
List<ProductResponse> responses = page.getRecords().stream()
.map(ProductAssembler::toResponse)
.collect(Collectors.toList());

return Result.success(new Page<>(responses, page.getTotal(), page.getPages()));
}

/**
* 扣减库存
*/
@PostMapping("/{id}/deduct-stock")
public Result<Boolean> deductStock(
@PathVariable("id") String productId,
@RequestParam Integer quantity) {
ProductId id = ProductId.of(productId);
boolean success = productApplicationService.deductStock(id, quantity);
return Result.success(success);
}
}

DTO 对象

@Data
public class ProductCreateRequest {
@NotBlank(message = "商品名称不能为空")
private String name;

@NotNull(message = "分类ID不能为空")
private Long categoryId;

@NotBlank(message = "价格不能为空")
private String price;

@NotNull(message = "库存不能为空")
private Integer stock;

private String description;
private List<String> images;
}

@Data
public class ProductResponse {
private String id;
private String name;
private Long categoryId;
private String categoryName;
private String price;
private Integer stock;
private Integer availableStock;
private String publishStatus;
private String newStatus;
private String recommendStatus;
private String description;
private List<String> images;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
}

@Data
@Builder
public class ProductQuery {
private String name;
private Long categoryId;
private Integer publishStatus;
private Integer newStatus;
private Integer pageNum;
private Integer pageSize;
}

统一响应结果

@Data
public class Result<T> {
private int code;
private String message;
private T data;

public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.setCode(200);
result.setMessage("success");
result.setData(data);
return result;
}

public static <T> Result<T> success() {
return success(null);
}

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

DDD核心概念体现

1. 聚合 (Aggregate)

  • Product聚合根管理商品的完整业务生命周期
  • 聚合边界内保证业务一致性
  • 通过聚合根对外提供统一的业务接口
  • 聚合根是业务一致性的边界,所有对聚合内部对象的修改都必须通过聚合根

2. 值对象 (Value Object)

@Getter
@EqualsAndHashCode
public class Price {
private final BigDecimal value;

private Price(BigDecimal value) {
// 业务约束验证
if (value == null || value.compareTo(BigDecimal.ZERO) < 0) {
throw new IllegalArgumentException("价格不能为负数");
}
this.value = value.setScale(2, BigDecimal.ROUND_HALF_UP);
}

// 业务行为
public Price add(Price other) {
return new Price(this.value.add(other.value));
}

public Price multiply(int quantity) {
return new Price(this.value.multiply(BigDecimal.valueOf(quantity)));
}
}

值对象特点

  • 不可变性:创建后不能修改
  • 值相等性:相同值即相等
  • 无唯一标识

3. 仓储模式 (Repository Pattern)

  • 仓储接口定义在领域层,实现放在基础设施层
  • 面向业务的查询方法,而非技术导向的CRUD
  • 隐藏数据访问的技术细节
  • 仓储不是DAO,它代表的是聚合根的集合

4. 领域服务 (Domain Service)

处理跨聚合的业务逻辑:

public void createProduct(ProductName name, CategoryId categoryId, Price price) {
// 业务规则验证
validateProductCreation(name, categoryId, price);

// 创建聚合根
Product product = new Product(name, categoryId, price);

// 持久化
productRepository.save(product);
}

领域服务特点

  • 处理不属于单个聚合的业务逻辑
  • 协调多个聚合或领域对象
  • 封装复杂的业务规则

5. 领域事件 (Domain Event)

public class ProductPublishedEvent extends DomainEvent {
private final ProductId productId;
private final ProductName productName;

public ProductPublishedEvent(ProductId productId, ProductName productName) {
this.productId = productId;
this.productName = productName;
}
}

领域事件作用

  • 实现事件驱动架构
  • 解耦业务流程
  • 实现最终一致性

业务流程示例

商品发布流程

1. 接口层: ProductController.publishProduct()

2. 应用层: ProductApplicationService.publishProduct()

3. 领域层: ProductDomainService.publishProduct()

4. 聚合根: Product.publish() (执行业务规则验证)

5. 仓储层: ProductRepository.save() (持久化状态变更)

商品创建流程

1. 接口层接收创建请求
2. 应用服务协调创建过程
3. 领域服务验证业务规则
4. 创建Product聚合根实例
5. 通过仓储保存到数据库
6. 返回聚合根标识

技术实现特点

1. 类型安全

  • 使用值对象确保数据一致性
  • 编译时就能发现类型错误
  • 避免原始类型带来的语义模糊

2. 业务导向

  • API设计面向业务场景而非技术实现
  • 查询方法体现业务意图
  • 异常信息具有业务含义

3. 分层解耦

  • 严格遵循依赖倒置原则
  • 上层不依赖下层具体实现
  • 通过接口定义契约

4. 可测试性

  • 领域逻辑独立于基础设施
  • 可以进行纯单元测试
  • 便于Mock和Stub

项目优势

✅ 符合DDD设计原则

  • 清晰的聚合边界
  • 丰富的值对象设计
  • 完整的分层架构
  • 面向业务的接口设计

✅ 良好的可维护性

  • 业务逻辑集中管理
  • 职责分离清晰
  • 易于扩展和修改
  • 降低系统复杂度

✅ 技术实现成熟

  • Spring Boot 3.0.2稳定版本
  • MyBatis Plus高效持久化
  • Java 17现代语言特性
  • 完善的异常处理机制

运行与测试

数据库表结构

-- 商品表
CREATE TABLE `t_product` (
`id` VARCHAR(36) NOT NULL COMMENT '商品ID',
`name` VARCHAR(200) NOT NULL COMMENT '商品名称',
`category_id` BIGINT NOT NULL COMMENT '分类ID',
`price` DECIMAL(10,2) NOT NULL COMMENT '价格',
`stock` INT NOT NULL DEFAULT 0 COMMENT '库存',
`reserved` INT NOT NULL DEFAULT 0 COMMENT '预留库存',
`warning_threshold` INT NOT NULL DEFAULT 10 COMMENT '预警阈值',
`publish_status` TINYINT NOT NULL DEFAULT 0 COMMENT '发布状态: 0-草稿 1-已发布 2-已下架',
`new_status` TINYINT NOT NULL DEFAULT 0 COMMENT '新品状态: 0-非新品 1-新品',
`recommend_status` TINYINT NOT NULL DEFAULT 0 COMMENT '推荐状态: 0-非推荐 1-推荐',
`description` TEXT COMMENT '商品描述',
`images` JSON COMMENT '商品图片JSON',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
`creator_id` BIGINT COMMENT '创建人ID',
`updater_id` BIGINT COMMENT '更新人ID',
`is_deleted` TINYINT NOT NULL DEFAULT 0 COMMENT '逻辑删除: 0-未删除 1-已删除',
PRIMARY KEY (`id`),
INDEX `idx_category_id` (`category_id`),
INDEX `idx_publish_status` (`publish_status`),
INDEX `idx_created_at` (`created_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='商品表';

-- 分类表
CREATE TABLE `t_category` (
`id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '分类ID',
`name` VARCHAR(64) NOT NULL COMMENT '分类名称',
`parent_id` BIGINT DEFAULT 0 COMMENT '父分类ID',
`level` TINYINT NOT NULL DEFAULT 1 COMMENT '层级',
`sort` INT NOT NULL DEFAULT 0 COMMENT '排序',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
`is_deleted` TINYINT NOT NULL DEFAULT 0,
PRIMARY KEY (`id`),
INDEX `idx_parent_id` (`parent_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='分类表';

启动项目

mvn spring-boot:run

API测试示例

# 创建商品
curl -X POST http://localhost:8003/api/products \
-H "Content-Type: application/json" \
-d '{
"name": "测试商品",
"categoryId": 1,
"price": "99.99",
"stock": 100
}'

# 发布商品
curl -X POST http://localhost:8003/api/products/{productId}/publish

# 查询商品
curl http://localhost:8003/api/products/{productId}

# 查询商品列表
curl "http://localhost:8003/api/products?pageNum=1&pageSize=10"

# 扣减库存
curl -X POST "http://localhost:8003/api/products/{productId}/deduct-stock?quantity=1"

DDD 设计原则总结

原则说明
聚合根边界聚合内对象保持强一致性,聚合间通过最终一致性
值对象不可变值对象创建后不能修改,任何修改返回新实例
仓储 vs DAO仓储是领域层的契约,DAO是基础设施层实现
领域事件解耦业务,实现事件驱动架构
依赖倒置领域层不依赖基础设施层,通过接口解耦
业务逻辑内聚业务规则应该存在于领域层,而非应用层或基础设施层

常见错误与反模式

反模式正确做法
在应用层写业务逻辑业务逻辑放在领域层或聚合根
贫血模型(只有getter/setter)充血模型,业务逻辑内聚在实体中
仓储做复杂查询使用查询服务或Specifications
领域对象暴露setter使用私有构造+工厂方法
忽略领域事件通过事件解耦跨聚合流程

总结

本项目完整展现了DDD的核心思想:

将复杂的业务逻辑封装在领域层,通过清晰的分层架构实现业务与技术的分离

这是一个优秀的DDD实践示例,适合学习和参考DDD设计模式在实际项目中的应用。