本文是《DeepSeek 辅助编程实战》系列第 3 篇,基于前两篇的需求和设计文档,演示如何用 DeepSeek 逐层实现 DDD 四层架构的后端代码。本文以 .NET Core (ASP.NET Core 8) 版本为例。

一、DDD 编码的核心原则

在开始编码之前,明确 DDD 的核心原则:

  1. 领域层不依赖任何框架:Entity、ValueObject、DomainService 都是纯业务代码
  2. 依赖倒置:领域层定义 Repository 接口,基础设施层实现
  3. 聚合根保护业务不变量:状态转换、业务校验都封装在聚合根内部
  4. 值对象不可变:使用 C# record 实现

二、逐层编码实战

第 1 层:领域层(Domain Layer)

2.1 枚举定义

我的 Prompt:

请基于以下状态机,用 C# 实现枚举和扩展方法:

状态:DRAFT → ISSUED → CHANGING → ISSUED(版本+1)
      任意状态 → CANCELLED

要求:
1. 枚举类型 PlanStatus
2. 扩展方法 CanTransitionTo() 验证状态转换合法性
3. 枚举值带中文显示名称

DeepSeek 生成的代码:

// Enums/PlanStatus.cs
public enum PlanStatus
{
    Draft = 0,      // 草稿
    Issued = 1,     // 已发布
    Changing = 2,   // 变更中
    Cancelled = 3   // 已取消
}

public static class PlanStatusExtensions
{
    public static bool CanTransitionTo(this PlanStatus from, PlanStatus to)
    {
        return (from, to) switch
        {
            (PlanStatus.Draft, PlanStatus.Issued) => true,
            (PlanStatus.Issued, PlanStatus.Changing) => true,
            (PlanStatus.Changing, PlanStatus.Issued) => true,
            (_, PlanStatus.Cancelled) => true,
            _ => false
        };
    }

    public static string GetDisplayName(this PlanStatus status)
    {
        return status switch
        {
            PlanStatus.Draft => "草稿",
            PlanStatus.Issued => "已发布",
            PlanStatus.Changing => "变更中",
            PlanStatus.Cancelled => "已取消",
            _ => status.ToString()
        };
    }
}
2.2 值对象

我的 Prompt:

请用 C# record 实现以下值对象:

1. Port(港口):unLocode(5位UN/LOCODE)、name、country、berth、
   estimatedArrival、estimatedDeparture
   - 构造函数校验 unLocode 格式(5位大写字母数字)
   - 提供 CalculatePortStayDuration() 计算在港停留时间

2. RouteSegment(航线段):fromPortCode、toPortCode、distanceNauticalMiles、
   estimatedSpeedKnots、estimatedDurationHours、sequence、
   estimatedFuelConsumptionIFO、estimatedFuelConsumptionMGO
   - 静态方法 CalculateFuelConsumption() 和 CalculateDuration()

使用 record 类型,确保不可变性。

DeepSeek 生成的 Port 值对象:

// ValueObjects/Port.cs
public record Port
{
    public string UnLocode { get; }
    public string Name { get; }
    public string Country { get; }
    public string? Berth { get; }
    public DateTime? EstimatedArrival { get; }
    public DateTime? EstimatedDeparture { get; }

    public Port(string unLocode, string name, string country,
                string? berth = null,
                DateTime? estimatedArrival = null,
                DateTime? estimatedDeparture = null)
    {
        if (string.IsNullOrWhiteSpace(unLocode) || unLocode.Length != 5)
            throw new ArgumentException("UN/LOCODE 必须为5位字符");
        if (string.IsNullOrWhiteSpace(name))
            throw new ArgumentException("港口名称不能为空");

        UnLocode = unLocode.ToUpper();
        Name = name;
        Country = country;
        Berth = berth;
        EstimatedArrival = estimatedArrival;
        EstimatedDeparture = estimatedDeparture;
    }

    public double? CalculatePortStayDuration()
    {
        if (EstimatedArrival == null || EstimatedDeparture == null)
            return null;
        return (EstimatedDeparture.Value - EstimatedArrival.Value).TotalHours;
    }
}
2.3 聚合根

我的 Prompt:

请实现 VoyagePlan 聚合根,要求:

1. 使用工厂方法 Create() 创建新计划
2. 状态转换方法:Issue()、InitiateChange()、CompleteChange()、Cancel()
3. CompleteChange() 时版本号+1,并克隆为新版本
4. 业务不变量:
   - 状态转换必须符合状态机规则
   - 发布前需校验完整性
5. 领域事件收集:每次状态变更记录 VoyagePlanEvent
6. 汇总计算:totalDistance 和 totalEstimatedHours 从航线段自动求和

用 C# 实现,包含完整的业务逻辑和校验。

DeepSeek 生成的聚合根(核心方法):

// Entities/VoyagePlan.cs
public class VoyagePlan
{
    public long Id { get; private set; }
    public string PlanNumber { get; private set; }
    public string VoyageName { get; private set; }
    public string VesselName { get; private set; }
    public string VoyageNumber { get; private set; }
    public PlanStatus Status { get; private set; }
    public int Version { get; private set; }

    // 值对象
    public Port DeparturePort { get; private set; }
    public Port DestinationPort { get; private set; }
    public List<RouteSegment> RouteSegments { get; private set; }
    public RiskAssessment RiskAssessment { get; private set; }
    public FuelEstimate FuelEstimate { get; private set; }
    public EmergencyPlan EmergencyPlan { get; private set; }

    // 汇总
    public decimal TotalDistance { get; private set; }
    public decimal TotalEstimatedHours { get; private set; }

    // 领域事件
    private readonly List<VoyagePlanEvent> _domainEvents = new();
    public IReadOnlyList<VoyagePlanEvent> DomainEvents => _domainEvents.AsReadOnly();

    // 工厂方法
    public static VoyagePlan Create(string voyageName, string vesselName,
        string voyageNumber, Port departurePort, Port destinationPort,
        List<RouteSegment> routeSegments, RiskAssessment riskAssessment,
        FuelEstimate fuelEstimate, EmergencyPlan emergencyPlan,
        string createdBy, string? remarks = null)
    {
        var plan = new VoyagePlan
        {
            VoyageName = voyageName,
            VesselName = vesselName,
            VoyageNumber = voyageNumber,
            Status = PlanStatus.Draft,
            Version = 1,
            DeparturePort = departurePort,
            DestinationPort = destinationPort,
            RouteSegments = routeSegments,
            RiskAssessment = riskAssessment,
            FuelEstimate = fuelEstimate,
            EmergencyPlan = emergencyPlan,
            CreatedBy = createdBy,
            Remarks = remarks,
            CreatedAt = DateTime.UtcNow,
        };
        plan.CalculateTotals();
        plan.RegisterEvent(VoyagePlanEventType.Created, createdBy, "创建航次计划");
        return plan;
    }

    // 发布
    public void Issue(string issuedBy)
    {
        EnsureStatus(PlanStatus.Draft);
        EnsureComplete();
        Status = PlanStatus.Issued;
        IssuedAt = DateTime.UtcNow;
        RegisterEvent(VoyagePlanEventType.Issued, issuedBy, "发布航次计划");
    }

    // 发起变更
    public void InitiateChange(string changedBy, string changeReason)
    {
        EnsureStatus(PlanStatus.Issued);
        Status = PlanStatus.Changing;
        RegisterEvent(VoyagePlanEventType.ChangeInitiated, changedBy,
            $"发起变更: {changeReason}");
    }

    // 完成变更 → 版本+1
    public void CompleteChange(string changedBy)
    {
        EnsureStatus(PlanStatus.Changing);
        Version++;
        Status = PlanStatus.Issued;
        RegisterEvent(VoyagePlanEventType.ChangeCompleted, changedBy,
            $"完成变更,升级至版本 V{Version}");
    }

    // 取消
    public void Cancel(string cancelledBy, string reason)
    {
        if (Status == PlanStatus.Cancelled)
            throw new BusinessException("计划已取消,不可重复取消");
        Status = PlanStatus.Cancelled;
        RegisterEvent(VoyagePlanEventType.Cancelled, cancelledBy,
            $"取消计划: {reason}");
    }

    private void EnsureStatus(PlanStatus expected)
    {
        if (Status != expected)
            throw new BusinessException(
                $"当前状态 {Status.GetDisplayName()} 不允许此操作,需要 {expected.GetDisplayName()} 状态");
    }

    private void EnsureComplete()
    {
        var missing = GetMissingFields();
        if (missing.Count > 0)
            throw new BusinessException($"计划不完整,缺少: {string.Join(", ", missing)}");
    }

    private void CalculateTotals()
    {
        TotalDistance = RouteSegments.Sum(s => s.DistanceNauticalMiles);
        TotalEstimatedHours = RouteSegments.Sum(s => s.EstimatedDurationHours);
    }
}
2.4 领域服务

我的 Prompt:

请实现 VoyagePlanDomainService,包含以下用例:

1. CreatePlanAsync:创建计划前检查船舶是否有活跃计划
2. IssuePlanAsync:发布计划
3. InitiateChangeAsync:发起变更
4. CompleteChangeAsync:完成变更
5. CancelPlanAsync:取消计划

领域服务负责协调聚合根和仓储,不包含业务逻辑。
// Services/VoyagePlanDomainService.cs
public class VoyagePlanDomainService
{
    private readonly IVoyagePlanRepository _repository;

    public async Task<VoyagePlan> CreatePlanAsync(/* 参数 */)
    {
        // 业务不变量:同一船舶只能有一个活跃计划
        bool hasActive = await _repository.HasActivePlanForVesselAsync(vesselName);
        if (hasActive)
            throw new BusinessException("该船舶已有活跃的航次计划");

        var plan = VoyagePlan.Create(/* ... */);
        var planNumber = await _repository.GeneratePlanNumberAsync();
        plan.SetPlanNumber(planNumber);

        await _repository.CreateAsync(plan);
        return plan;
    }
}

第 2 层:应用层(Application Layer)

我的 Prompt:

请实现应用层,包含:

1. DTO 定义:CreateVoyagePlanRequest、VoyagePlanResponse、UpdateVoyagePlanRequest
2. 应用服务 VoyagePlanApplicationService:
   - 11 个用例方法
   - DTO ↔ Entity 转换
   - 委托领域服务处理业务逻辑
   - 统一异常处理(BusinessException → 400)
// Services/VoyagePlanApplicationService.cs
public class VoyagePlanApplicationService
{
    private readonly IVoyagePlanRepository _repository;
    private readonly VoyagePlanDomainService _domainService;

    public async Task<VoyagePlanResponse> CreatePlanAsync(CreateVoyagePlanRequest request)
    {
        // DTO → 领域对象
        var departurePort = new Port(request.DeparturePort.UnLocode, ...);
        var routeSegments = request.RouteSegments.Select(s => new RouteSegment(...)).ToList();

        // 委托领域服务
        var plan = await _domainService.CreatePlanAsync(
            request.VoyageName, request.VesselName, ...);

        // Entity → DTO
        return VoyagePlanResponse.FromEntity(plan);
    }
}

第 3 层:基础设施层(Infrastructure Layer)

我的 Prompt:

请用 Entity Framework Core 实现:

1. VoyagePlanDbContext:使用 Fluent API 配置
   - 值对象配置为 Owned 类型
   - RouteSegments 和 TransitPorts 序列化为 JSON 列
   - 忽略 DomainEvents(不持久化)

2. VoyagePlanRepository:实现 IVoyagePlanRepository
   - 计划编号生成:VP-YYYYMMDD-NNN
   - 活跃计划检查
// Persistence/VoyagePlanDbContext.cs
public class VoyagePlanDbContext : DbContext
{
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        var entity = modelBuilder.Entity<VoyagePlan>();

        // 值对象配置为 Owned
        entity.OwnsOne(p => p.DeparturePort);
        entity.OwnsOne(p => p.DestinationPort);
        entity.OwnsOne(p => p.RiskAssessment);
        entity.OwnsOne(p => p.FuelEstimate);
        entity.OwnsOne(p => p.EmergencyPlan);

        // 集合值对象序列化为 JSON
        entity.Property(p => p.RouteSegments)
              .HasColumnType("text")
              .HasConversion(
                  v => JsonSerializer.Serialize(v, _jsonOptions),
                  v => JsonSerializer.Deserialize<List<RouteSegment>>(v, _jsonOptions)!
              );

        // 忽略领域事件
        entity.Ignore(p => p.DomainEvents);
    }
}

第 4 层:API 层

我的 Prompt:

请实现 ASP.NET Core API 层:

1. Program.cs:DI 注册、中间件配置、Swagger
2. VoyagePlanController:11 个 Action
3. GlobalExceptionMiddleware:统一异常处理

要求:
- 使用 Minimal API 风格或 Controller 风格均可
- Swagger 文档分组
- CORS 配置
// Controllers/VoyagePlanController.cs
[ApiController]
[Route("api/v1/voyage-plans")]
public class VoyagePlanController : ControllerBase
{
    private readonly VoyagePlanApplicationService _service;

    [HttpPost]
    public async Task<IActionResult> Create([FromBody] CreateVoyagePlanRequest request)
    {
        var result = await _service.CreatePlanAsync(request);
        return CreatedAtAction(nameof(GetById), new { id = result.Id }, result);
    }

    [HttpPost("{id}/issue")]
    public async Task<IActionResult> Issue(long id, [FromBody] IssueRequest request)
    {
        var result = await _service.IssuePlanAsync(id, request.IssuedBy);
        return Ok(result);
    }
}

三、Prompt 技巧

1. 按层逐步生成,不要一次生成全部代码

第 1 轮:领域层(枚举 → 值对象 → 聚合根 → 领域服务)
第 2 轮:应用层(DTO → 应用服务)
第 3 轮:基础设施层(DbContext → Repository)
第 4 轮:API 层(Controller → Middleware → DI)

2. 提供设计文档作为上下文

基于以下领域模型设计(见附录),请用 C# 实现聚合根。
[粘贴第 2 篇博客的领域模型输出]

3. 指定编码规范

要求:
- 使用 C# 12 语法特性(record、pattern matching)
- 属性使用 init-only setter
- 方法命名使用 PascalCase
- 私有字段使用 _camelCase
- 包含 XML 注释

4. 让 AI 生成单元测试

请为 VoyagePlanDomainService 编写单元测试,覆盖:
1. 创建计划成功
2. 创建计划失败(船舶有活跃计划)
3. 发布计划
4. 发起变更
5. 完成变更(版本+1)
6. 取消计划

使用 xUnit + Moq。

四、编码阶段的输出物

经过 4 轮编码对话,我们得到了:

  1. 领域层:3 个枚举 + 6 个值对象 + 1 个聚合根 + 1 个领域事件 + 1 个领域服务 + 1 个仓储接口
  2. 应用层:5 个 DTO + 1 个应用服务 + 1 个自定义异常
  3. 基础设施层:1 个 DbContext + 1 个 Repository 实现
  4. API 层:1 个 Controller + 1 个中间件 + DI 配置
  5. 测试:6 个单元测试用例

总计约 20+ 个 C# 文件,完整的 DDD 四层架构实现。

五、注意事项

  1. 代码审查不可省略:AI 生成的代码可能有编译错误或逻辑漏洞,需要逐行审查
  2. 编译验证:每生成一层代码就编译一次,及时发现类型不匹配等问题
  3. 测试驱动:先让 AI 写测试,再写实现,确保业务逻辑正确

下一篇预告

下一篇《用 DeepSeek 做前端开发》将演示如何用 DeepSeek 实现 Vue 3 + Element Plus 前端页面,包括列表页、详情页、创建表单和编辑表单。

Logo

欢迎加入DeepSeek 技术社区。在这里,你可以找到志同道合的朋友,共同探索AI技术的奥秘。

更多推荐