领域驱动设计架构详解
项目概述
这是一个基于**领域驱动设计(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设计模式在实际项目中的应用。