/
afanasevn
/
MaxSystems
Обзор
Документация
Войти
/
afanasevn
/
MaxSystems
Код
Запросы
0
Пакеты
0
Релизы
0
Аналитика
Безопасность
master
src/MaxSystems.Infrastructure/Services/WorkService.cs
351 строка
12 KB
IBS\NAfanasev
Integration Event Bus
22 июн 2026, 17:02
22 июн 2026, 17:02
05d97a7
Код
Авторство
О чём код?
using MaxSystems.Application.Abstractions; using MaxSystems.Application.Contracts.Works; using MaxSystems.Application.Integration; using MaxSystems.Application.Integration.Events; using MaxSystems.Domain.Entities; using MaxSystems.Domain.Enums; using MaxSystems.Infrastructure.Audit; using MaxSystems.Infrastructure.Common; using MaxSystems.Infrastructure.Data; using Microsoft.EntityFrameworkCore; namespace MaxSystems.Infrastructure.Services; /// <summary>Жизненный цикл работ: CRUD, чек-листы, audit и интеграционные события.</summary> public sealed class WorkService( AppDbContext db, IAuditService audit, IIntegrationEventPublisher integrationEvents) : IWorkService { private static readonly WorkStatus[] ClosedStatuses = [WorkStatus.Closed, WorkStatus.Cancelled, WorkStatus.Done]; /// <inheritdoc /> public async Task<IReadOnlyList<WorkListItemDto>> ListAsync( WorkStatus? status = null, EquipmentSystem? system = null, WorkType? type = null, bool? overdue = null, CancellationToken ct = default) { var today = DateTime.UtcNow.Date; var query = db.Works.AsQueryable(); if (status.HasValue) query = query.Where(x => x.Status == status.Value); if (system.HasValue) query = query.Where(x => x.System == system.Value); if (type.HasValue) query = query.Where(x => x.Type == type.Value); if (overdue == true) { query = query.Where(x => x.PlannedDate < today && !ClosedStatuses.Contains(x.Status)); } return await query .OrderByDescending(x => x.PlannedDate) .Select(x => new WorkListItemDto( x.Id, x.Number, x.Title, x.Type, x.System, x.Status, x.PlannedDate, x.Priority, x.Equipment != null ? x.Equipment.Name : null, x.PlannedDate < today && !ClosedStatuses.Contains(x.Status))) .ToListAsync(ct); } /// <inheritdoc /> public async Task<WorkDetailDto?> GetByIdAsync( Guid id, CancellationToken ct = default) { var today = DateTime.UtcNow.Date; var work = await db.Works .AsNoTracking() .Include(x => x.Equipment) .Include(x => x.LeadSpecialist) .Include(x => x.Assignee) .Include(x => x.Checklist) .Include(x => x.Materials) .ThenInclude(x => x.Item) .FirstOrDefaultAsync(x => x.Id == id, ct); if (work is null) return null; ChecklistDto? checklist = work.Checklist is null ? null : new ChecklistDto( work.Checklist.Type, work.Checklist.ItemsJson, work.Checklist.SummaryJson, work.Checklist.Completed); var materials = work.Materials .Select(m => new WorkMaterialItemDto(m.ItemId, m.Item.Name, m.Quantity, m.Notes)) .ToList(); return new WorkDetailDto( work.Id, work.Number, work.ServiceDeskTicket, work.Type, work.Category, work.System, work.EquipmentId, work.Equipment?.Name, work.Priority, work.LeadSpecialistId, work.LeadSpecialist?.Name, work.AssigneeId, work.Assignee?.Name, work.PlannedDate, work.ActualDate, work.Status, work.WaitReason, work.Title, work.Description, work.Result, work.NeedsPurchase, work.NeedsRework, work.Recommendations, work.PlannedDate < today && !ClosedStatuses.Contains(work.Status), checklist, materials); } /// <inheritdoc /> public async Task<WorkListItemDto> CreateAsync( CreateWorkRequest request, Guid? actorUserId = null, CancellationToken ct = default) { var count = await db.Works.CountAsync(ct); var number = WorkNumberGenerator.Generate(count + 1); var status = request.AssigneeId.HasValue ? WorkStatus.Assigned : WorkStatus.New; var work = new Work { Id = Guid.NewGuid(), Number = number, Title = request.Title, Type = request.Type, Category = request.Category, System = request.System, Priority = request.Priority, EquipmentId = request.EquipmentId, ServiceDeskTicket = request.ServiceDeskTicket, LeadSpecialistId = request.LeadSpecialistId, AssigneeId = request.AssigneeId, PlannedDate = request.PlannedDate, Description = request.Description, Status = status, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }; db.Works.Add(work); audit.Log( actorUserId, "CREATE", "Work", work.Id, AuditMessages.WorkCreated(work.Number, work.Title, work.Type), new { work.Number, work.Title, Type = work.Type.ToString(), Status = work.Status.ToString() }); await integrationEvents.EnqueueAsync( new WorkCreatedIntegrationEvent( work.Id, work.Number, work.AssigneeId, work.LeadSpecialistId, work.Title), IntegrationEventTypes.WorkCreated, IntegrationRoutingKeys.WorkCreated, IntegrationQueueRoutes.WorkLifecycle, workId: work.Id, payloadSummary: $"Работа {work.Number}: {work.Title}", ct: ct); if (request.AssigneeId.HasValue) { await integrationEvents.EnqueueAsync( new WorkAssigneeChangedIntegrationEvent( work.Id, work.Number, PreviousAssigneeId: null, NewAssigneeId: request.AssigneeId), IntegrationEventTypes.WorkAssigneeChanged, IntegrationRoutingKeys.WorkAssigneeChanged, IntegrationQueueRoutes.WorkLifecycle, workId: work.Id, payloadSummary: $"Работа {work.Number}: назначен исполнитель", ct: ct); } await db.SaveChangesAsync(ct); return new WorkListItemDto( work.Id, work.Number, work.Title, work.Type, work.System, work.Status, work.PlannedDate, work.Priority); } /// <inheritdoc /> public async Task<WorkListItemDto?> UpdateStatusAsync( Guid id, UpdateWorkStatusRequest request, Guid? actorUserId = null, CancellationToken ct = default) { var work = await db.Works .Include(x => x.Equipment) .ThenInclude(x => x!.Regulation) .FirstOrDefaultAsync(x => x.Id == id, ct); if (work is null) return null; var previousStatus = work.Status; work.Status = request.Status; work.WaitReason = request.WaitReason; work.Result = request.Result; work.UpdatedAt = DateTime.UtcNow; if (request.Status is WorkStatus.Done or WorkStatus.Closed or WorkStatus.OnReview) work.ActualDate = DateTime.UtcNow; audit.Log( actorUserId, "UPDATE", "Work", work.Id, AuditMessages.WorkStatusChanged(work.Number, previousStatus, request.Status), new { work.Number, FromStatus = previousStatus.ToString(), ToStatus = request.Status.ToString(), request.WaitReason, }); await integrationEvents.EnqueueAsync( new WorkStatusChangedIntegrationEvent( work.Id, work.Number, previousStatus, request.Status, work.AssigneeId), IntegrationEventTypes.WorkStatusChanged, IntegrationRoutingKeys.WorkStatusChanged(request.Status), IntegrationQueueRoutes.WorkLifecycle, workId: work.Id, payloadSummary: $"Работа {work.Number}: {previousStatus} => {request.Status}", ct: ct); await db.SaveChangesAsync(ct); if (request.Status == WorkStatus.Closed && work.Type == WorkType.Maintenance && work.Equipment is not null) { var equipmentName = work.Equipment.Name; var equipmentId = work.Equipment.Id; await MaintenanceService.CompleteMaintenanceAsync( db, id, DateTime.UtcNow.AddDays(work.Equipment.Regulation?.IntervalDays ?? 180), ct); audit.Log( actorUserId, "UPDATE", "Equipment", equipmentId, AuditMessages.MaintenanceDatesUpdated(equipmentName), new { EquipmentName = equipmentName, WorkNumber = work.Number }); await db.SaveChangesAsync(ct); } return new WorkListItemDto( work.Id, work.Number, work.Title, work.Type, work.System, work.Status, work.PlannedDate, work.Priority); } /// <inheritdoc /> public async Task SaveChecklistAsync( Guid workId, SaveChecklistRequest request, Guid? actorUserId = null, CancellationToken ct = default) { var work = await db.Works.FirstOrDefaultAsync(x => x.Id == workId, ct) ?? throw new InvalidOperationException("Работа не найдена"); var existing = await db.ChecklistResponses .FirstOrDefaultAsync(x => x.WorkId == workId, ct); if (existing is null) { db.ChecklistResponses.Add(new ChecklistResponse { Id = Guid.NewGuid(), WorkId = workId, Type = request.Type, ItemsJson = request.ItemsJson, SummaryJson = request.SummaryJson, Completed = request.Completed, CreatedAt = DateTime.UtcNow, UpdatedAt = DateTime.UtcNow }); } else { existing.Type = request.Type; existing.ItemsJson = request.ItemsJson; existing.SummaryJson = request.SummaryJson; existing.Completed = request.Completed; existing.UpdatedAt = DateTime.UtcNow; } if (request.Completed) { var previousStatus = work.Status; work.Status = WorkStatus.OnReview; work.UpdatedAt = DateTime.UtcNow; await integrationEvents.EnqueueAsync( new WorkStatusChangedIntegrationEvent( work.Id, work.Number, previousStatus, WorkStatus.OnReview, work.AssigneeId), IntegrationEventTypes.WorkStatusChanged, IntegrationRoutingKeys.WorkStatusChanged(WorkStatus.OnReview), IntegrationQueueRoutes.WorkLifecycle, workId: work.Id, payloadSummary: $"Работа {work.Number}: {previousStatus} => {WorkStatus.OnReview}", ct: ct); } audit.Log( actorUserId, "UPDATE", "ChecklistResponse", workId, AuditMessages.ChecklistSaved(work.Number, request.Completed), new { work.Number, request.Type, request.Completed }); await db.SaveChangesAsync(ct); } }