| ¶Ô±ÈÐÂÎļþ |
| | |
| | | # 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 èåæ ¹è®¾è®¡ |
| | | |
| | | ```csharp |
| | | // 设å¤èåæ ¹ |
| | | 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 å¼å¯¹è±¡è®¾è®¡ |
| | | |
| | | ```csharp |
| | | // 设å¤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 é¢åæå¡è®¾è®¡ |
| | | |
| | | ```csharp |
| | | // 设å¤è°åº¦é¢åæå¡ |
| | | 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 ä»å¨æ¥å£è®¾è®¡ |
| | | |
| | | ```csharp |
| | | // 设å¤ä»å¨æ¥å£ |
| | | 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 åºç¨æå¡è®¾è®¡ |
| | | |
| | | ```csharp |
| | | // 设å¤åºç¨æå¡ |
| | | 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 ä»å¨å®ç° |
| | | |
| | | ```csharp |
| | | // 设å¤ä»å¨å®ç° |
| | | 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 é信驱å¨å°è£
|
| | | |
| | | ```csharp |
| | | // 设å¤éä¿¡éé
卿¥å£ |
| | | 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 ç¼åä¼å |
| | | |
| | | ```csharp |
| | | // å¤çº§ç¼åçç¥ |
| | | 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 æ°æ®åºæ¥è¯¢ä¼å |
| | | |
| | | ```csharp |
| | | // æ¹éæ¥è¯¢ä¼å |
| | | 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 ç»ä¸å¼å¸¸å¤ç |
| | | |
| | | ```csharp |
| | | // é¢åå¼å¸¸åºç±» |
| | | 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 ç»æåæ¥å¿ |
| | | |
| | | ```csharp |
| | | // é¢åäºä»¶æ¥å¿ |
| | | 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. é£é©è¯ä¼° |
| | | |
| | | | é£é© | å¯è½æ§ | å½±å | ç¼è§£æªæ½ | |
| | | |------|--------|------|----------| |
| | | | æ¶é´ä¸è¶³ | é« | é« | åé¶æ®µå®æ½ï¼ä¼å
æ ¸å¿åè½ | |
| | | | æ°æ®è¿ç§»é®é¢ | ä¸ | é« | å
åæµè¯ï¼ä¿çåæ»æ¹æ¡ | |
| | | | æ§è½ä¸é | ä¸ | ä¸ | æ§è½åºåæµè¯ï¼çæ§ä¼å | |
| | | | å¢ééåº | ä½ | ä¸ | æä¾å¹è®ï¼ææ¡£å®å | |