# ddd-mall-may **Repository Path**: raikay/ddd-mall-may ## Basic Information - **Project Name**: ddd-mall-may - **Description**: No description available - **Primary Language**: Unknown - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 0 - **Created**: 2026-03-02 - **Last Updated**: 2026-03-22 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 订单管理系统 - DDD 领域驱动设计教程 ## 1. 什么是领域驱动设计 (DDD) 领域驱动设计(Domain-Driven Design)是一种软件开发方法论,强调通过领域模型来解决复杂的业务问题。DDD 的核心思想是: - **以业务为中心**:将业务逻辑和业务规则放在软件的核心位置 - **Ubiquitous Language(通用语言)**:开发人员和业务人员使用相同的术语进行沟通 - **分层架构**:将系统分为不同的层次,每层职责明确 - **领域模型**:通过实体、值对象、聚合等概念建模业务领域 ## 2. 项目分层架构 本项目采用经典的 DDD 分层架构: ``` OrderSystem.Api (表现层) ↓ OrderSystem.Application (应用层) ↓ OrderSystem.Domain (领域层) ↓ OrderSystem.Infrastructure (基础设施层) ``` --- ## 3. 领域层 (Domain Layer) 领域层是 DDD 的核心,包含了业务逻辑和业务规则。 ### 3.1 实体 (Entity) **位置**:`OrderSystem.Domain/Entities/Order.cs` **代码示例**: ```csharp public class Order { public Guid Id { get; private set; } = Guid.NewGuid(); public string OrderNumber { get; private set; } = string.Empty; public string Status { get; private set; } = OrderStatus.Pending; public ICollection Items { get; private set; } = new List(); public static Order Create(string orderNumber, Address shippingAddress, ...) { return new Order { ... }; } public void AddItem(Guid productId, string productName, Money unitPrice, int quantity) { // 领域逻辑:只有待支付订单才能添加订单项 if (Status != OrderStatus.Pending) throw new InvalidOperationException("只有待支付订单才能添加订单项"); var item = OrderItem.Create(Id, productId, productName, unitPrice, quantity); Items.Add(item); CalculateAmounts(); } public void Cancel() { if (Status == OrderStatus.Cancelled) throw new InvalidOperationException("订单已取消"); UpdateStatus(OrderStatus.Cancelled); CancelledAt = DateTime.UtcNow; } } ``` **为什么使用实体**: - **唯一标识**:每个订单都有唯一的 `Id`,用于区分不同的订单 - **生命周期管理**:订单从创建到完成有一个完整的生命周期 - **业务规则封装**:订单的状态转换规则(如只有待支付订单才能取消)封装在实体内部 ### 3.2 值对象 (Value Object) **位置**:`OrderSystem.Domain/ValueObjects/Address.cs` 和 `Money.cs` **代码示例**: ```csharp public class Address { public string Street { get; private set; } = string.Empty; public string City { get; private set; } = string.Empty; public string State { get; private set; } = string.Empty; public string ZipCode { get; private set; } = string.Empty; public string Country { get; private set; } = string.Empty; public static Address Create(string street, string city, string state, string zipCode, string country) { return new Address { ... }; } } public class Money { public decimal Amount { get; private set; } public static Money Create(decimal amount) { return new Money { Amount = amount }; } public static Money operator +(Money left, Money right) { return Money.Create(left.Amount + right.Amount); } } ``` **为什么使用值对象**: - **不可变性**:值对象创建后不能修改,保证数据一致性 - **无标识性**:地址和金额只关心值,不关心身份 - **可共享性**:相同的地址和金额可以在多个订单中共享 - **业务规则封装**:Money 类封装了金额计算的业务规则 ### 3.3 领域服务 (Domain Service) **位置**:`OrderSystem.Domain/Services/IOrderServiceDomain.cs` 和 `OrderServiceDomain.cs` **代码示例**: ```csharp public interface IOrderServiceDomain { bool CanCancel(Order order); bool CanShip(Order order); void ValidateOrderForCreation(Order order); void ValidateOrderForCancellation(Order order); } public class OrderServiceDomain : IOrderServiceDomain { public bool CanCancel(Order order) { // 领域逻辑:只有待支付和已支付状态的订单可以取消 return order.Status == OrderStatus.Pending || order.Status == OrderStatus.Paid; } public void ValidateOrderForCreation(Order order) { // 领域验证:订单必须包含至少一个商品 if (order.Items == null || !order.Items.Any()) throw new OrderException("订单必须包含至少一个商品"); // 领域验证:订单项信息必须有效 foreach (var item in order.Items) { if (item.Quantity <= 0) throw new OrderException($"商品 {item.ProductName} 的数量必须大于0"); } } } ``` **为什么使用领域服务**: - **跨实体业务逻辑**:订单状态转换规则涉及多个实体的状态 - **业务规则集中管理**:将订单相关的业务规则集中在一个服务中 - **避免实体膨胀**:将复杂的业务逻辑从实体中分离出来 ### 3.4 聚合根 (Aggregate Root) **位置**:`OrderSystem.Domain/Entities/Order.cs` **代码示例**: ```csharp public class Order { public Guid Id { get; private set; } = Guid.NewGuid(); public string OrderNumber { get; private set; } = string.Empty; public ICollection Items { get; private set; } = new List(); // 聚合根方法:确保聚合内部一致性 public void AddItem(Guid productId, string productName, Money unitPrice, int quantity) { // 验证订单状态 if (Status != OrderStatus.Pending) throw new InvalidOperationException("只有待支付订单才能添加订单项"); // 创建订单项 var item = OrderItem.Create(Id, productId, productName, unitPrice, quantity); Items.Add(item); // 重新计算订单金额 CalculateAmounts(); } private void CalculateAmounts() { // 计算商品总额 Subtotal = Items.Sum(item => item.Subtotal); // 计算订单总额 TotalAmount = Subtotal + ShippingFee + TaxAmount - DiscountAmount; } } ``` **为什么使用聚合根**: - **一致性边界**:Order 是聚合根,OrderItem 是聚合内部的实体 - **保证内部一致性**:通过聚合根方法确保订单和订单项的一致性 - **简化外部访问**:外部只能通过聚合根访问聚合内部的实体 ### 3.5 领域异常 (Domain Exception) **位置**:`OrderSystem.Domain/Exceptions/` **代码示例**: ```csharp public class OrderException : Exception { public OrderException(string message) : base(message) { } } public class ProductException : Exception { public ProductException(string message) : base(message) { } } public class InventoryException : Exception { public InventoryException(string message) : base(message) { } } ``` **为什么使用领域异常**: - **业务语义清晰**:异常名称直接反映业务问题 - **错误处理精确**:可以针对不同的业务异常进行不同的处理 - **代码可读性**:异常类型本身就是一种文档 --- ## 4. 应用层 (Application Layer) 应用层负责协调领域层,处理应用逻辑,不包含业务规则。 ### 4.1 应用服务 (Application Service) **位置**:`OrderSystem.Application/Services/OrderService.cs` **代码示例**: ```csharp public class OrderService : IOrderService { private readonly IOrderRepository _orderRepository; private readonly IProductRepository _productRepository; private readonly IOrderServiceDomain _orderDomainService; private readonly IUnitOfWork _unitOfWork; public async Task CreateOrderAsync(CreateOrderRequest request, CancellationToken cancellationToken = default) { // 1. 解析收货地址 var addressParts = request.ShippingAddress.Split('|'); Address address = Address.Create(...); // 2. 创建订单实体 var order = Order.Create(...); // 3. 添加订单项 foreach (var itemRequest in request.Items) { var product = await _productRepository.GetByIdAsync(itemRequest.ProductId, cancellationToken); order.AddItem(itemRequest.ProductId, product.Name, product.Price, itemRequest.Quantity); } // 4. 验证订单 _orderDomainService.ValidateOrderForCreation(order); // 5. 保存订单 await _orderRepository.AddAsync(order, cancellationToken); // 6. 提交事务 await _unitOfWork.CommitAsync(cancellationToken); return order.ToDto(); } public async Task CancelOrderAsync(Guid orderId, CancellationToken cancellationToken = default) { var order = await _orderRepository.GetByIdAsync(orderId, cancellationToken); // 验证订单是否可以取消 _orderDomainService.ValidateOrderForCancellation(order); // 更新订单状态 order.Cancel(); // 释放库存 foreach (var item in order.Items) { await _inventoryService.ReleaseInventoryAsync(item.ProductId, item.Quantity); } // 提交事务 await _unitOfWork.CommitAsync(cancellationToken); return order.ToDto(); } } ``` **为什么使用应用服务**: - **协调领域对象**:应用服务负责协调多个领域对象完成业务用例 - **事务管理**:通过工作单元管理事务边界 - **DTO 转换**:将领域对象转换为应用层使用的 DTO - **不包含业务规则**:业务规则在领域层,应用服务只负责协调 ### 4.2 数据传输对象 (DTO) **位置**:`OrderSystem.Application/DTOs/` **代码示例**: ```csharp public class OrderDto { public Guid Id { get; set; } public string OrderNumber { get; set; } = string.Empty; public string Status { get; set; } = string.Empty; public decimal TotalAmount { get; set; } public List Items { get; set; } = new List(); } public class CreateOrderRequest { public string? OrderNumber { get; set; } public string? ShippingAddress { get; set; } public string? CustomerId { get; set; } public string? CustomerName { get; set; } public string? CustomerPhone { get; set; } public List Items { get; set; } = new List(); } ``` **为什么使用 DTO**: - **解耦领域层和应用层**:DTO 与领域实体分离,避免耦合 - **网络传输优化**:只传输需要的数据 - **版本控制**:DTO 可以独立于领域模型进行版本控制 ### 4.3 领域事件 (Domain Event) **位置**:`OrderSystem.Domain/Events/` **代码示例**: ```csharp public record OrderCreatedEvent(Order Order) : IDomainEvent; public record OrderCancelledEvent(Order Order) : IDomainEvent; public record OrderShippedEvent(Order Order) : IDomainEvent; ``` **为什么使用领域事件**: - **解耦业务逻辑**:订单创建后触发通知、积分等后续操作 - **扩展性**:可以轻松添加新的事件处理器 - **业务语义清晰**:事件名称直接反映业务发生的事情 --- ## 5. 基础设施层 (Infrastructure Layer) 基础设施层负责实现领域层定义的接口,提供技术实现。 ### 5.1 仓储接口 (Repository Interface) **位置**:`OrderSystem.Domain/Repositories/IOrderRepository.cs` **代码示例**: ```csharp public interface IOrderRepository { Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default); Task> GetAllAsync(CancellationToken cancellationToken = default); Task> GetByCustomerIdAsync(string customerId, CancellationToken cancellationToken = default); Task> GetByStatusAsync(string status, CancellationToken cancellationToken = default); Task AddAsync(Order order, CancellationToken cancellationToken = default); Task UpdateAsync(Order order); Task RemoveAsync(Order order, CancellationToken cancellationToken = default); } ``` **为什么使用仓储接口**: - **抽象数据访问**:领域层不依赖具体的数据访问技术 - **测试友好**:可以轻松 mocking 仓储接口进行单元测试 - **符合依赖倒置原则**:高层模块不依赖低层模块 ### 5.2 仓储实现 (Repository Implementation) **位置**:`OrderSystem.Infrastructure/Repositories/OrderRepository.cs` **代码示例**: ```csharp public class OrderRepository : IOrderRepository { private readonly OrderSystemDbContext _context; public OrderRepository(OrderSystemDbContext context) { _context = context; } public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) { return await _context.Orders .Include(o => o.Items) .FirstOrDefaultAsync(o => o.Id == id, cancellationToken); } public async Task AddAsync(Order order, CancellationToken cancellationToken = default) { await _context.Orders.AddAsync(order, cancellationToken); } public async Task UpdateAsync(Order order) { _context.Orders.Update(order); } } ``` **为什么使用仓储实现**: - **技术实现**:使用 Entity Framework Core 实现数据访问 - **查询优化**:使用 Include 加载关联数据 - **统一数据访问**:所有数据访问逻辑集中在仓储中 ### 5.3 工作单元 (Unit of Work) **位置**:`OrderSystem.Infrastructure/UnitOfWork/UnitOfWork.cs` **代码示例**: ```csharp public class UnitOfWork : IUnitOfWork { private readonly OrderSystemDbContext _context; private IProductRepository? _productRepository; private IOrderRepository? _orderRepository; private bool _committed = false; public IProductRepository Products => _productRepository ??= new ProductRepository(_context); public IOrderRepository Orders => _orderRepository ??= new OrderRepository(_context); public async Task CommitAsync(CancellationToken cancellationToken = default) { if (_committed) throw new InvalidOperationException("事务已提交,无法再次提交"); int result = await _context.SaveChangesAsync(cancellationToken); _committed = true; return result; } public async Task RollbackAsync() { _committed = true; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } } ``` **为什么使用工作单元**: - **事务管理**:确保多个仓储操作在同一个事务中执行 - **统一提交**:通过工作单元统一提交所有更改 - **延迟加载**:仓储只有在第一次访问时才创建 - **资源管理**:实现 IDisposable 接口确保资源释放 **工作单元的核心作用**: 1. **保证数据一致性**:多个仓储操作要么全部成功,要么全部失败 2. **减少数据库往返**:将多个操作合并为一个事务 3. **简化事务管理**:开发者不需要手动管理事务 ### 5.4 数据上下文 (DbContext) **位置**:`OrderSystem.Infrastructure/Data/OrderSystemDbContext.cs` **代码示例**: ```csharp public class OrderSystemDbContext : DbContext { public DbSet Orders { get; set; } = null!; public DbSet OrderItems { get; set; } = null!; public DbSet Products { get; set; } = null!; public OrderSystemDbContext(DbContextOptions options) : base(options) { } protected override void OnModelCreating(ModelBuilder modelBuilder) { // 配置订单实体 modelBuilder.Entity(entity => { entity.HasKey(o => o.Id); entity.Property(o => o.OrderNumber).IsRequired().HasMaxLength(50); entity.Property(o => o.Status).IsRequired().HasMaxLength(20); entity.OwnsOne(o => o.ShippingAddress, address => { address.Property(a => a.Street).HasColumnName("ShippingStreet"); address.Property(a => a.City).HasColumnName("ShippingCity"); address.Property(a => a.State).HasColumnName("ShippingState"); address.Property(a => a.ZipCode).HasColumnName("ShippingZipCode"); address.Property(a => a.Country).HasColumnName("ShippingCountry"); }); entity.HasMany(o => o.Items).WithOne().HasForeignKey(i => i.OrderId); }); // 配置订单项实体 modelBuilder.Entity(entity => { entity.HasKey(i => i.Id); entity.Property(i => i.ProductName).IsRequired().HasMaxLength(200); }); // 配置商品实体 modelBuilder.Entity(entity => { entity.HasKey(p => p.Id); entity.Property(p => p.Name).IsRequired().HasMaxLength(200); entity.Property(p => p.Price).HasColumnType("decimal(18,2)"); }); } } ``` **为什么使用数据上下文**: - **ORM 映射**:将领域模型映射到数据库表 - **变更跟踪**:自动跟踪实体的变更 - **LINQ 查询**:使用 LINQ 进行类型安全的查询 --- ## 6. DDD 核心概念总结 ### 6.1 实体 vs 值对象 | 特性 | 实体 (Entity) | 值对象 (Value Object) | |------|--------------|---------------------| | 标识 | 有唯一标识 (Id) | 无标识,只关心值 | | 可变性 | 可变 | 不可变 | | 生命周期 | 有生命周期 | 无生命周期 | | 示例 | Order, OrderItem | Address, Money | **选择原则**: - 如果对象需要唯一标识 → 使用实体 - 如果对象只关心值,不关心身份 → 使用值对象 ### 6.2 聚合设计 **订单聚合**: ``` Order (聚合根) ├── OrderItem (聚合内部实体) │ ├── Product (引用实体) │ └── UnitPrice (值对象) └── ShippingAddress (值对象) ``` **设计原则**: - 聚合根负责维护聚合内部的一致性 - 聚合内部实体只能通过聚合根访问 - 聚合之间通过 ID 引用,避免直接引用 ### 6.3 领域服务 vs 应用服务 | 特性 | 领域服务 (Domain Service) | 应用服务 (Application Service) | |------|-------------------------|------------------------------| | 职责 | 包含业务规则 | 协调领域对象 | | 位置 | 领域层 | 应用层 | | 事务 | 不管理事务 | 管理事务边界 | | 依赖 | 不依赖基础设施 | 依赖仓储和领域服务 | **选择原则**: - 业务规则封装在领域服务中 - 用例协调在应用服务中 ### 6.4 仓储模式 **仓储接口**: ```csharp public interface IOrderRepository { Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default); Task AddAsync(Order order, CancellationToken cancellationToken = default); Task UpdateAsync(Order order); } ``` **仓储实现**: ```csharp public class OrderRepository : IOrderRepository { private readonly OrderSystemDbContext _context; public async Task GetByIdAsync(Guid id, CancellationToken cancellationToken = default) { return await _context.Orders.FindAsync(id, cancellationToken); } } ``` **优势**: - 领域层不依赖具体技术 - 易于测试和替换实现 - 统一数据访问入口 --- ## 7. DDD 实践建议 ### 7.1 通用语言 (Ubiquitous Language) - 所有代码中的类名、方法名使用业务术语 - 开发人员和业务人员使用相同的术语 - 避免技术术语污染领域模型 ### 7.2 贫血模型 vs 充血模型 **本项目采用充血模型**: ```csharp // 充血模型:业务逻辑在领域对象中 public class Order { public void AddItem(Guid productId, string productName, Money unitPrice, int quantity) { // 领域逻辑 } } // 贫血模型:业务逻辑在服务中(不推荐) public class Order { public Guid Id { get; set; } public string Status { get; set; } } public class OrderService { public void AddItem(Order order, ...) { } // 业务逻辑在这里 } ``` ### 7.3 领域层不依赖基础设施 - 领域层只包含业务逻辑,不依赖数据库、消息队列等 - 通过接口抽象技术细节 - 使用依赖注入解耦 ### 7.4 事务管理 - 使用工作单元管理事务 - 事务边界在应用服务中 - 确保聚合的一致性 --- ## 8. 总结 本项目完整地实践了 DDD 的核心概念: 1. **领域层**:包含实体、值对象、领域服务、聚合根等核心业务逻辑 2. **应用层**:协调领域对象,处理应用逻辑,管理事务 3. **基础设施层**:实现领域层定义的接口,提供技术实现 4. **表现层**:暴露 API,处理 HTTP 请求 通过 DDD,我们实现了: - **高内聚低耦合**:业务逻辑集中在领域层 - **易维护性**:代码结构清晰,职责明确 - **易测试性**:各层可以独立测试 - **业务对齐**:代码直接反映业务需求 --- 基于对仓库内全部 4 个项目、约 30 个 `.cs` 文件及项目引用的阅读,结论如下。 --- ## 总体结论:**采用 DDD 分层与核心战术元素,但不算“严格、一致”的 DDD 落地** 项目在**结构**和**命名**上明显按 DDD 教程组织,领域模型也有一定行为,但**依赖方向、事务边界、跨聚合一致性**和**部分封装**与经典 DDD 仍有差距。 --- ## 与 DDD 一致的部分 1. **分层清晰(与 `readme.md` 一致)** - `OrderSystem.Api`:HTTP / 表现层 - `OrderSystem.Application`:用例编排、DTO、`IUnitOfWork` - `OrderSystem.Domain`:实体、值对象、仓储接口、领域服务接口、领域异常 - `OrderSystem.Infrastructure`:EF、仓储实现、部分“领域服务”实现 2. **依赖规则(大体正确)** - `Domain` 无项目引用,无 EF — 符合“领域纯净”。 - `Application` 仅引用 `Domain`。 - `Api` 引用 `Application` + `Infrastructure`(组合根)— 常见做法。 3. **战术模式有体现** - **实体 + 行为**:`Order`、`Product` 有工厂方法 `Create`、业务方法(如 `AddItem`、`Cancel`、`DecreaseStock`),不是纯数据类。 - **值对象**:`Money`、`Address`。 - **仓储抽象**:`IOrderRepository`、`IProductRepository` 在领域层。 - **应用服务**:`OrderService` / `ProductService` 协调仓储与领域服务。 - **领域异常**:`OrderException`、`ProductException` 等。 - **聚合意图**:注释与结构上将 `Order` + `OrderItem` 作为订单聚合。 --- ## 与 DDD / 整洁架构不够一致或存在风险的部分 ### 1. `Infrastructure` 引用 `Application` `OrderSystem.Infrastructure.csproj` 引用了 `Application`,原因是 `InventoryDomainService`、`UnitOfWork` 使用了 `OrderSystem.Application.UnitOfWork.IUnitOfWork`。 更常见的做法是:把 **`IUnitOfWork` 放在 `Domain`(或与仓储并列的抽象端口)**,让 **`Infrastructure` 只依赖 `Domain`**,避免基础设施层依赖应用层。 ### 2. “领域服务”的职责与位置混乱 - 接口 `IOrderDomainService` 在 `Domain`,实现 `OrderDomainService` 在 `Infrastructure`,且逻辑基本是**纯规则校验**,却注入了 `IOrderRepository` / `IProductRepository` 却**未使用**(多余依赖、也容易误导读者)。 - 实现类上的 `CanCancel`、`CanShip`、`GenerateOrderNumber` 等**未出现在接口上**,与 `readme.md` 中的示例也不一致,说明**契约与文档、实现未对齐**。 严格来说:**无外部依赖的领域规则**更适合放在 `Domain` 内(纯领域服务或实体/领域模型上),需要仓储的才放在基础设施并实现领域接口。 ### 3. 事务与工作单元边界(与 DDD 一致性强相关) `InventoryDomainService` 在 `DeductInventoryAsync` / `ReleaseInventoryAsync` 内部调用 `await _unitOfWork.CommitAsync()`,而 `OrderService` 在取消订单等流程末尾**再次** `CommitAsync`。 在同一 Scoped `DbContext` / `IUnitOfWork` 下,这很容易导致: - 多次提交、或 - 第二次 `CommitAsync` 时因 `UnitOfWork` 已标记提交而抛错(见 `UnitOfWork.CommitAsync` 中对 `_committed` 的检查)。 从 DDD 角度,**一个用例应有一个清晰的一致性边界**;跨聚合(订单 + 库存)的提交应由**应用层在同一工作单元内**完成,而不是在“领域服务”里各自 `Commit`。 ### 4. 跨聚合业务不完整(库存) `CreateOrderAsync` 中只做了**库存校验**,未见对 `IInventoryDomainService.DeductInventoryAsync` 的调用;取消订单时却会 `ReleaseInventoryAsync`。 这会造成**领域规则不对称**(下单不占库存、取消却释放),也不符合多数电商领域的**预留/扣减库存**模型。 ### 5. 聚合封装不完整 `Order.DiscountAmount` 为 **`public set`**,应用层可直接赋值,绕开实体上的不变式与领域方法,**削弱聚合根对内部状态的封装**(与 DDD 中“通过领域方法修改状态”的习惯不一致)。 ### 6. 其他小点 - `OrderStatus` 使用 **字符串常量**而非强类型枚举/值对象,容易出现非法状态字符串(依赖约定而非类型)。 - `OrderService.Create` 里 `Order.Create(..., request.CustomerEmail, request.CustomerPhone)` 的实参疑似把 **邮箱与电话传错位置**(需对照 `Create` 签名核对),属于业务正确性问题,也会破坏“通用语言”的一致性。 --- ## 文件覆盖说明 已覆盖:全部 `.csproj`、全部 `.cs` 源文件及 `readme.md` 中与架构相关的章节;**未**把 `appsettings`、SQLite 文件等当作 DDD 依据。 --- ## 一句话总结 **项目在“分层 + 实体/值对象/仓储/应用服务”层面是在复用 DDD 模式的教学型实现;若按严格 DDD 衡量,在依赖方向、工作单元与跨聚合事务、库存与订单一致性、以及聚合封装上仍有明显改进空间。** 若你希望,我可以在不扩大范围的前提下,按优先级给出一份**仅针对上述问题的重构顺序**(仍用中文、且对应到具体文件)。