编辑 | blame | 历史 | 原始文档

WCS系统渐进式DDD重构设计文档

创建日期: 2026-03-11
版本: 1.0
范围: 全系统重构(渐进式DDD)

1. 架构总览

基于您的需求,我将提出一个适合2周时间窗口的渐进式DDD重构方案。

┌─────────────────────────────────────────────────────────────┐
│                        表现层 (API)                           │
│  WIDESEAWCS_Server (ASP.NET Core Controllers)                │
└─────────────────────────┬───────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────┐
│                        应用层                                 │
│  应用服务 / DTO / 命令 / 查询                                  │
└─────────────────────────┬───────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────┐
│                        领域层 (新增)                          │
│  ┌───────────────────────────────────────────────────────┐   │
│  │  设备管理领域                                          │   │
│  │  • 聚合根: 设备, 设备组, 设备任务                     │   │
│  │  • 实体: 设备状态, 设备位置, 设备属性                 │   │
│  │  • 值对象: 设备ID, 地址, 状态枚举                    │   │
│  │  • 领域服务: 设备调度器, 状态机                      │   │
│  │  • 仓储接口: IDeviceRepository, IDeviceTaskRepository │   │
│  └───────────────────────────────────────────────────────┘   │
│  ┌───────────────────────────────────────────────────────┐   │
│  │  仓储作业领域                                          │   │
│  │  • 聚合根: 作业, 托盘, 库位                            │   │
│  │  • 领域事件: 作业完成, 作业异常                        │   │
│  └───────────────────────────────────────────────────────┘   │
│  ┌───────────────────────────────────────────────────────┐   │
│  │  系统集成领域                                          │   │
│  │  • 集成服务: WMS集成, MES集成, ERP集成               │   │
│  └───────────────────────────────────────────────────────┘   │
└─────────────────────────┬───────────────────────────────────┘
                          │
                          ▼
┌─────────────────────────────────────────────────────────────┐
│                      基础设施层                              │
│  • 仓储实现 (SqlSugar ORM)                                   │
│  • 通信驱动 (HslCommunication)                               │
│  • 缓存服务 (Redis)                                          │
│  • 任务调度 (Quartz.NET)                                     │
│  • 数据库访问 (SqlSugar)                                     │
└─────────────────────────────────────────────────────────────┘

2. 领域层设计 - 设备管理领域(核心)

2.1 聚合根设计

// 设备聚合根
public class Device : AggregateRoot<DeviceId>
{
    // 基本属性
    private DeviceId _id;
    private DeviceName _name;
    private DeviceType _type;
    private DeviceStatus _status;
    private DeviceAddress _address;
    private List<DeviceProperty> _properties;

    // 状态管理
    private DateTime _lastConnectedAt;
    private DateTime _lastHeartbeatAt;
    private string _errorMessage;

    // 领域事件
    private List<IDomainEvent> _domainEvents = new();

    // 行为方法
    public void Connect()
    {
        if (_status == DeviceStatus.Connected)
            throw new DomainException("设备已连接");

        _status = DeviceStatus.Connected;
        _lastConnectedAt = DateTime.UtcNow;
        _domainEvents.Add(new DeviceConnectedEvent(_id));
    }

    public void Disconnect(string reason)
    {
        _status = DeviceStatus.Disconnected;
        _errorMessage = reason;
        _domainEvents.Add(new DeviceDisconnectedEvent(_id, reason));
    }

    public void UpdateHeartbeat()
    {
        _lastHeartbeatAt = DateTime.UtcNow;
        _domainEvents.Add(new DeviceHeartbeatEvent(_id));
    }

    public void SetProperty(string key, string value)
    {
        var property = _properties.FirstOrDefault(p => p.Key == key);
        if (property != null)
            property.UpdateValue(value);
        else
            _properties.Add(DeviceProperty.Create(key, value));
    }

    public IReadOnlyCollection<IDomainEvent> GetDomainEvents() => _domainEvents.AsReadOnly();
    public void ClearDomainEvents() => _domainEvents.Clear();
}

// 设备组聚合根
public class DeviceGroup : AggregateRoot<DeviceGroupId>
{
    private DeviceGroupId _id;
    private DeviceGroupName _name;
    private List<DeviceId> _deviceIds;
    private GroupStrategy _strategy;
    private int _currentIndex;

    public void AddDevice(DeviceId deviceId)
    {
        if (_deviceIds.Contains(deviceId))
            throw new DomainException("设备已在组内");
        _deviceIds.Add(deviceId);
    }

    public void RemoveDevice(DeviceId deviceId)
    {
        _deviceIds.Remove(deviceId);
    }

    // 轮询策略
    public DeviceId GetNextDevice()
    {
        if (!_deviceIds.Any())
            throw new DomainException("设备组为空");

        switch (_strategy)
        {
            case GroupStrategy.RoundRobin:
                return _deviceIds[_currentIndex++ % _deviceIds.Count];
            case GroupStrategy.Random:
                return _deviceIds[new Random().Next(_deviceIds.Count)];
            default:
                return _deviceIds[0];
        }
    }
}

// 设备任务聚合根
public class DeviceTask : AggregateRoot<DeviceTaskId>
{
    private DeviceTaskId _id;
    private DeviceId _deviceId;
    private TaskType _type;
    private TaskStatus _status;
    private TaskPriority _priority;
    private TaskPayload _payload;
    private DateTime _createdAt;
    private DateTime? _startedAt;
    private DateTime? _completedAt;
    private string? _errorMessage;
    private List<IDomainEvent> _domainEvents = new();

    public void Start()
    {
        if (_status != TaskStatus.Pending)
            throw new DomainException("任务状态不正确");

        _status = TaskStatus.Running;
        _startedAt = DateTime.UtcNow;
        _domainEvents.Add(new TaskStartedEvent(_id, _deviceId));
    }

    public void Complete()
    {
        if (_status != TaskStatus.Running)
            throw new DomainException("任务未在运行中");

        _status = TaskStatus.Completed;
        _completedAt = DateTime.UtcNow;
        _domainEvents.Add(new TaskCompletedEvent(_id, _deviceId));
    }

    public void Fail(string errorMessage)
    {
        _status = TaskStatus.Failed;
        _errorMessage = errorMessage;
        _completedAt = DateTime.UtcNow;
        _domainEvents.Add(new TaskFailedEvent(_id, _deviceId, errorMessage));
    }

    public IReadOnlyCollection<IDomainEvent> GetDomainEvents() => _domainEvents.AsReadOnly();
    public void ClearDomainEvents() => _domainEvents.Clear();
}

2.2 值对象设计

// 设备ID值对象
public record DeviceId(Guid Value)
{
    public static DeviceId New() => new DeviceId(Guid.NewGuid());
    public static DeviceId From(Guid value) => new DeviceId(value);
}

// 设备名称值对象
public record DeviceName(string Value)
{
    public DeviceName(string value)
    {
        if (string.IsNullOrWhiteSpace(value) || value.Length > 100)
            throw new ArgumentException("设备名称无效");
        Value = value;
    }
}

// 设备类型枚举
public enum DeviceType
{
    StackerCrane,    // 堆垛机
    ConveyorLine,    // 输送线
    ShuttleCar,      // 穿梭车
    Robot,           // 机械手
    AGV              // 自动导引车
}

// 设备状态枚举
public enum DeviceStatus
{
    Disconnected,    // 未连接
    Connecting,      // 连接中
    Connected,       // 已连接
    Busy,            // 忙碌
    Error,           // 错误
    Maintenance      // 维护中
}

// 设备地址值对象
public record DeviceAddress(string Ip, int Port)
{
    public DeviceAddress(string ip, int port)
    {
        if (!IPAddress.TryParse(ip, out _))
            throw new ArgumentException("IP地址无效");
        if (port < 1 || port > 65535)
            throw new ArgumentException("端口无效");
        Ip = ip;
        Port = port;
    }
}

2.3 领域服务设计

// 设备调度领域服务
public interface IDeviceScheduler
{
    Task<DeviceTask> CreateTask(DeviceId deviceId, TaskType type, TaskPayload payload);
    Task<DeviceTask> AssignTask(DeviceId deviceId);
    Task CompleteTask(DeviceTaskId taskId);
    Task FailTask(DeviceTaskId taskId, string errorMessage);
}

public class DeviceScheduler : IDeviceScheduler
{
    private readonly IDeviceTaskRepository _taskRepository;
    private readonly IDeviceRepository _deviceRepository;
    private readonly ITaskQueue _taskQueue;

    public async Task<DeviceTask> CreateTask(DeviceId deviceId, TaskType type, TaskPayload payload)
    {
        var device = await _deviceRepository.GetById(deviceId);
        if (device == null)
            throw new DomainException("设备不存在");

        if (device.Status != DeviceStatus.Connected)
            throw new DomainException("设备未连接");

        var task = DeviceTask.Create(deviceId, type, payload);
        await _taskRepository.Add(task);
        await _taskQueue.Enqueue(task);

        return task;
    }

    public async Task<DeviceTask> AssignTask(DeviceId deviceId)
    {
        var tasks = await _taskRepository.GetPendingTasksForDevice(deviceId);
        if (!tasks.Any())
            return null;

        var task = tasks.OrderBy(t => t.Priority).First();
        task.Start();
        await _taskRepository.Update(task);

        return task;
    }
}

// 设备状态机领域服务
public interface IDeviceStateMachine
{
    bool CanTransition(DeviceStatus current, DeviceStatus target);
    void Transition(Device device, DeviceStatus target);
}

public class DeviceStateMachine : IDeviceStateMachine
{
    private static readonly Dictionary<DeviceStatus, HashSet<DeviceStatus>> _transitions = new()
    {
        [DeviceStatus.Disconnected] = new() { DeviceStatus.Connecting, DeviceStatus.Maintenance },
        [DeviceStatus.Connecting] = new() { DeviceStatus.Connected, DeviceStatus.Error },
        [DeviceStatus.Connected] = new() { DeviceStatus.Busy, DeviceStatus.Disconnected, DeviceStatus.Error, DeviceStatus.Maintenance },
        [DeviceStatus.Busy] = new() { DeviceStatus.Connected, DeviceStatus.Error },
        [DeviceStatus.Error] = new() { DeviceStatus.Disconnected, DeviceStatus.Maintenance },
        [DeviceStatus.Maintenance] = new() { DeviceStatus.Disconnected }
    };

    public bool CanTransition(DeviceStatus current, DeviceStatus target)
    {
        return _transitions.TryGetValue(current, out var allowed) && allowed.Contains(target);
    }

    public void Transition(Device device, DeviceStatus target)
    {
        if (!CanTransition(device.Status, target))
            throw new DomainException($"无效的状态转换: {device.Status} -> {target}");

        device.SetStatus(target);
    }
}

2.4 仓储接口设计

// 设备仓储接口
public interface IDeviceRepository : IRepository<Device, DeviceId>
{
    Task<Device?> GetByName(DeviceName name);
    Task<IEnumerable<Device>> GetByType(DeviceType type);
    Task<IEnumerable<Device>> GetByStatus(DeviceStatus status);
    Task<IEnumerable<Device>> GetAllConnected();
}

// 设备任务仓储接口
public interface IDeviceTaskRepository : IRepository<DeviceTask, DeviceTaskId>
{
    Task<IEnumerable<DeviceTask>> GetPendingTasksForDevice(DeviceId deviceId);
    Task<IEnumerable<DeviceTask>> GetRunningTasksForDevice(DeviceId deviceId);
    Task<IEnumerable<DeviceTask>> GetTasksByStatus(TaskStatus status);
}

3. 应用层设计

3.1 应用服务设计

// 设备应用服务
public class DeviceApplicationService
{
    private readonly IDeviceRepository _deviceRepository;
    private readonly IDeviceScheduler _deviceScheduler;
    private readonly IUnitOfWork _unitOfWork;

    public async Task<DeviceDto> GetDevice(DeviceId id)
    {
        var device = await _deviceRepository.GetById(id);
        return device.ToDto();
    }

    public async Task<DeviceTaskDto> CreateTask(CreateDeviceTaskCommand command)
    {
        await using var transaction = await _unitOfWork.BeginTransaction();

        var deviceId = DeviceId.From(command.DeviceId);
        var task = await _deviceScheduler.CreateTask(
            deviceId,
            command.Type,
            new TaskPayload(command.Payload)
        );

        await _unitOfWork.Commit();
        return task.ToDto();
    }

    public async Task CompleteTask(CompleteDeviceTaskCommand command)
    {
        var taskId = DeviceTaskId.From(command.TaskId);
        await _deviceScheduler.CompleteTask(taskId);
    }
}

// 命令和查询对象
public record CreateDeviceTaskCommand(Guid DeviceId, TaskType Type, string Payload);
public record CompleteDeviceTaskCommand(Guid TaskId);

// DTO
public record DeviceDto(Guid Id, string Name, DeviceType Type, DeviceStatus Status);
public record DeviceTaskDto(Guid Id, Guid DeviceId, TaskType Type, TaskStatus Status);

4. 基础设施层重构

4.1 仓储实现

// 设备仓储实现
public class DeviceRepository : IDeviceRepository
{
    private readonly ISqlSugarClient _db;
    private readonly ICacheService _cache;

    public DeviceRepository(ISqlSugarClient db, ICacheService cache)
    {
        _db = db;
        _cache = cache;
    }

    public async Task<Device?> GetById(DeviceId id)
    {
        // 先查缓存
        var cached = await _cache.Get<Device>($"device:{id.Value}");
        if (cached != null)
            return cached;

        // 查数据库
        var entity = await _db.Queryable<Dt_DeviceInfo>()
            .Where(d => d.DeviceId == id.Value)
            .FirstAsync();

        if (entity == null) return null;

        // 转换为领域模型
        var device = MapToDomain(entity);

        // 写入缓存
        await _cache.Set($"device:{id.Value}", device, TimeSpan.FromMinutes(5));

        return device;
    }

    public async Task Add(Device device)
    {
        var entity = MapToEntity(device);
        await _db.Insertable(entity).ExecuteCommandAsync();

        // 清除相关缓存
        await _cache.RemoveByPrefix("device:");
    }

    private Device MapToDomain(Dt_DeviceInfo entity) { /* ... */ }
    private Dt_DeviceInfo MapToEntity(Device domain) { /* ... */ }
}

4.2 通信驱动封装

// 设备通信适配器接口
public interface IDeviceCommunicatorAdapter
{
    Task<bool> Connect(DeviceAddress address);
    Task<bool> Disconnect();
    Task<DeviceData> ReadData(string address);
    Task WriteData(string address, object value);
    Task<bool> IsConnected();
}

// Siemens PLC适配器
public class SiemensCommunicatorAdapter : IDeviceCommunicatorAdapter
{
    private readonly SiemensS7Net _plc;

    public SiemensCommunicatorAdapter(DeviceAddress address)
    {
        _plc = new SiemensS7Net(SiemensPLCS.S1200, address.Ip);
        _plc.ConnectTimeOut = 5000;
    }

    public async Task<bool> Connect(DeviceAddress address)
    {
        var result = await Task.Run(() => _plc.ConnectServer());
        return result.IsSuccess;
    }

    public async Task<DeviceData> ReadData(string address)
    {
        var result = await Task.Run(() => _plc.Read(address));
        if (!result.IsSuccess)
            throw new CommunicationException($"读取失败: {result.Message}");

        return new DeviceData(result.Content);
    }

    // ... 其他方法
}

5. 分层清晰化

5.1 项目结构重组

WIDESEAWCS_Domain/
├── DeviceManagement/
│   ├── Aggregates/
│   │   ├── Device.cs
│   │   ├── DeviceGroup.cs
│   │   └── DeviceTask.cs
│   ├── Entities/
│   │   ├── DeviceStatus.cs
│   │   ├── DeviceLocation.cs
│   │   └── DeviceProperty.cs
│   ├── ValueObjects/
│   │   ├── DeviceId.cs
│   │   ├── DeviceName.cs
│   │   ├── DeviceAddress.cs
│   │   └── TaskPayload.cs
│   ├── Services/
│   │   ├── IDeviceScheduler.cs
│   │   ├── DeviceScheduler.cs
│   │   ├── IDeviceStateMachine.cs
│   │   └── DeviceStateMachine.cs
│   ├── Repositories/
│   │   ├── IDeviceRepository.cs
│   │   └── IDeviceTaskRepository.cs
│   └── Events/
│       ├── DeviceConnectedEvent.cs
│       └── TaskCompletedEvent.cs
└── WarehouseOperations/
    └── ...

WIDESEAWCS_Application/
├── Services/
│   └── DeviceApplicationService.cs
├── Commands/
│   └── CreateDeviceTaskCommand.cs
├── Queries/
│   └── GetDeviceQuery.cs
└── DTOs/
    └── DeviceDto.cs

WIDESEAWCS_Infrastructure/
├── Persistence/
│   ├── Repositories/
│   │   ├── DeviceRepository.cs
│   │   └── DeviceTaskRepository.cs
│   └── SqlSugar/
│       └── DbContext.cs
├── Communication/
│   ├── Adapters/
│   │   ├── SiemensCommunicatorAdapter.cs
│   │   ├── OmronCommunicatorAdapter.cs
│   │   └── ModbusCommunicatorAdapter.cs
│   └── IDeviceCommunicatorAdapter.cs
├── Caching/
│   └── RedisCacheService.cs
└── Scheduling/
    └── QuartzJobManager.cs

WIDESEAWCS_Server/
└── Controllers/
    └── DeviceController.cs

6. 性能优化策略

6.1 缓存优化

// 多级缓存策略
public class HybridCacheService : ICacheService
{
    private readonly IMemoryCache _l1Cache;
    private readonly IConnectionMultiplexer _redis;
    private readonly IDistributedCache _l2Cache;

    public async Task<T?> Get<T>(string key)
    {
        // L1缓存
        if (_l1Cache.TryGetValue(key, out T l1Value))
            return l1Value;

        // L2缓存
        var l2Value = await _l2Cache.GetStringAsync(key);
        if (l2Value != null)
        {
            var value = JsonSerializer.Deserialize<T>(l2Value);
            _l1Cache.Set(key, value, TimeSpan.FromMinutes(1));
            return value;
        }

        return default;
    }

    public async Task Set<T>(string key, T value, TimeSpan? expiry = null)
    {
        _l1Cache.Set(key, value, TimeSpan.FromMinutes(1));

        var serialized = JsonSerializer.Serialize(value);
        await _l2Cache.SetStringAsync(key, serialized, expiry);
    }
}

6.2 数据库查询优化

// 批量查询优化
public class DeviceRepository : IDeviceRepository
{
    public async Task<IDictionary<DeviceId, Device>> GetByIds(IEnumerable<DeviceId> ids)
    {
        var idList = ids.Select(id => id.Value).ToList();

        // 批量查询
        var entities = await _db.Queryable<Dt_DeviceInfo>()
            .In(idList)
            .ToListAsync();

        return entities.ToDictionary(
            e => DeviceId.From(e.DeviceId),
            e => MapToDomain(e)
        );
    }

    // 延迟查询
    public async Task<IEnumerable<Device>> QueryActiveDevices()
    {
        return await _db.Queryable<Dt_DeviceInfo>()
            .Where(d => d.Status != DeviceStatus.Disconnected)
            .OrderBy(d => d.Name)
            .Select(d => MapToDomain(d))
            .ToListAsync();
    }
}

7. 错误处理和日志统一

7.1 统一异常处理

// 领域异常基类
public abstract class DomainException : Exception
{
    public string ErrorCode { get; }

    protected DomainException(string message, string errorCode = "DOMAIN_ERROR")
        : base(message)
    {
        ErrorCode = errorCode;
    }
}

// 应用异常
public class ApplicationException : Exception
{
    public string ErrorCode { get; }

    public ApplicationException(string message, string errorCode = "APP_ERROR")
        : base(message)
    {
        ErrorCode = errorCode;
    }
}

// 全局异常处理中间件
public class GlobalExceptionHandlerMiddleware
{
    public async Task InvokeAsync(HttpContext context, RequestDelegate next)
    {
        try
        {
            await next(context);
        }
        catch (DomainException ex)
        {
            context.Response.StatusCode = 400;
            await context.Response.WriteAsJsonAsync(new
            {
                error = ex.ErrorCode,
                message = ex.Message
            });
        }
        catch (ApplicationException ex)
        {
            context.Response.StatusCode = 500;
            await context.Response.WriteAsJsonAsync(new
            {
                error = ex.ErrorCode,
                message = ex.Message
            });
        }
    }
}

7.2 结构化日志

// 领域事件日志
public class DomainEventLogger : IDomainEventDispatcher
{
    private readonly ILogger<DomainEventLogger> _logger;

    public async Task Dispatch(IDomainEvent domainEvent)
    {
        _logger.LogInformation("领域事件: {EventType}, 数据: {EventData}",
            domainEvent.GetType().Name,
            JsonSerializer.Serialize(domainEvent));

        // 发送到事件总线
    }
}

// 设备操作日志
public class DeviceOperationLogger : IDeviceOperationLogger
{
    public void LogConnect(DeviceId deviceId, bool success, string? error = null)
    {
        if (success)
        {
            _logger.LogInformation("设备连接成功: {DeviceId}", deviceId.Value);
        }
        else
        {
            _logger.LogError("设备连接失败: {DeviceId}, 错误: {Error}",
                deviceId.Value, error);
        }
    }
}

8. 实施计划(2周)

第1周:领域层搭建

天数 任务
Day 1-2 创建领域项目结构,定义值对象和聚合根
Day 3-4 实现领域服务和状态机
Day 5 创建仓储接口,定义领域事件

第2周:应用层和基础设施层

天数 任务
Day 1-2 实现应用服务和DTO
Day 3 创建仓储实现,迁移数据访问逻辑
Day 4 封装通信驱动,优化缓存策略
Day 5 统一错误处理和日志,集成测试

9. 设计决策记录

9.1 为什么选择渐进式DDD而非完整DDD重构?

决策:选择渐进式DDD重构而非完整DDD重构

理由
1. 时间约束:2周内无法完成完整DDD重构
2. 风险控制:渐进式降低风险,可分阶段验证
3. 系统稳定性:保持现有功能可用
4. 资源效率:优先重构核心领域

9.2 为什么将设备管理作为核心领域?

决策:将设备管理作为第一个重构的核心领域

理由
1. 业务优先级:设备管理是WCS系统的核心
2. 代码复杂度:设备管理包含状态、调度、通信等复杂逻辑
3. 优化空间大:设备管理存在较多性能和耦合问题
4. 影响范围广:设备管理是其他业务的基础

9.3 为什么采用CQRS模式?

决策:在应用层采用CQRS模式

理由
1. 关注点分离:命令和查询职责清晰
2. 性能优化:可分别优化读写操作
3. 扩展性好:便于添加缓存、事件处理


10. 非功能性需求

10.1 性能要求

  • 设备状态查询响应时间 < 100ms
  • 任务创建响应时间 < 200ms
  • 支持至少100个并发设备连接

10.2 可维护性要求

  • 代码覆盖率 > 80%
  • 圈复杂度 < 10
  • 遵循单一职责原则

10.3 可扩展性要求

  • 支持新增设备类型无需修改核心代码
  • 支持新增任务类型
  • 支持多种通信协议扩展

11. 风险评估

风险 可能性 影响 缓解措施
时间不足 分阶段实施,优先核心功能
数据迁移问题 充分测试,保留回滚方案
性能下降 性能基准测试,监控优化
团队适应 提供培训,文档完善