using System; using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; using NPP.SmartSchedue.Api.Contracts.Core.Enums; using NPP.SmartSchedue.Api.Contracts.Services.Notification; using NPP.SmartSchedue.Api.Contracts.Services.Notification.Input; using NPP.SmartSchedue.Api.Contracts.Services.Notification.Output; using NPP.SmartSchedue.Api.Contracts.Services.Work; using NPP.SmartSchedue.Api.Services.Personnel; using ZhonTai.Admin.Core.GrpcServices; using ZhonTai.Admin.Services; using ZhonTai.DynamicApi; using ZhonTai.DynamicApi.Attributes; namespace NPP.SmartSchedue.Api.Services.Notification; /// /// 通知服务 定时消息 /// [DynamicApi(Area = "app")] public class NotificationScheduledService : BaseService, IDynamicApi { private readonly ILogger _logger; private readonly IWorkOrderService _workOrderService; private readonly INotificationService _notificationService; private readonly IUserGrpcService _userService; private readonly PersonnelQualificationService _personnelQualificationService; public NotificationScheduledService(IWorkOrderService workOrderService, INotificationService notificationService, IUserGrpcService userService, ILogger logger, PersonnelQualificationService personnelQualificationService) { _logger = logger; _workOrderService = workOrderService; _userService = userService; _notificationService = notificationService; _personnelQualificationService = personnelQualificationService; } /// /// 待提交任务提醒 /// /// [HttpGet] [AllowAnonymous] public async Task CheckWorkOrderByPendingSubmit() { var setting = await GetNotificationSettingCache("任务待提交提醒"); if (setting == null || !setting.IsEnabled) { _logger.LogWarning("任务待提交提醒未查询到有效的通知配置或已禁用"); return new OkObjectResult(new { success = true, sent = 0, skipped = 0 }); } if (!IsWithinTimeWindow(setting, DateTime.Now)) { _logger.LogInformation("当前不在通知时间窗内,跳过发送"); return new OkObjectResult(new { success = true, sent = 0, skipped = 0 }); } var recipientIds = await GetNotificationPersonListAsync(setting); if (recipientIds == null || recipientIds.Count == 0) { _logger.LogInformation("人员组无成员,跳过发送"); return new OkObjectResult(new { success = true, sent = 0, skipped = 0 }); } var pendingByCreator = await _workOrderService.GetPendingSubmitCountByCreatorAsync(); if (pendingByCreator == null || pendingByCreator.Count == 0) { _logger.LogInformation("无待提交任务,跳过发送"); return new OkObjectResult(new { success = true, sent = 0 }); } var intersectRecipients = pendingByCreator.Distinct().ToList(); if (intersectRecipients.Count == 0) { _logger.LogInformation("人员组成员中无待提交任务创建人,跳过发送"); return new OkObjectResult(new { success = true, sent = 0 }); } var now = DateTime.Now; var sendInputs = new List(); foreach (var personId in intersectRecipients) { var lastSentAt = await GetLastSentAt(setting.Id, personId.Key, "WorkOrderPendingSubmit"); if (!ShouldResendForPerson(setting, now, lastSentAt)) { continue; } var count = pendingByCreator.TryGetValue(personId.Key, out var c) ? c : 0; if (count <= 0) continue; var variables = new Dictionary { ["UserId"] = personId.ToString(), ["PendingCount"] = count.ToString(), ["Now"] = now.ToString("yyyy-MM-dd HH:mm"), ["count"] = count.ToString(), }; var sysTitle = string.IsNullOrWhiteSpace(setting.SystemMessageTitleTemplate) ? "任务待提交提醒" : await Render(setting.SystemMessageTitleTemplate, variables); var sysContent = string.IsNullOrWhiteSpace(setting.SystemMessageContentTemplate) ? $"您有{count}个任务处于待提交状态,请及时处理" : await Render(setting.SystemMessageContentTemplate, variables); if (setting.IsSystemMessageEnabled) { sendInputs.Add(new SendNotificationInput { SettingId = setting.Id, NotificationType = NotificationTypeEnum.SystemMessage, Title = sysTitle, Content = sysContent, RecipientPersonnelIds = new List { personId.Key }, BusinessType = "WorkOrderPendingSubmit", BusinessId = null, BusinessData = "", SendImmediately = true }); } if (setting.IsEmailEnabled) { var emailSubject = string.IsNullOrWhiteSpace(setting.EmailSubjectTemplate) ? sysTitle : await Render(setting.EmailSubjectTemplate, variables); var emailContent = string.IsNullOrWhiteSpace(setting.EmailContentTemplate) ? sysContent : await Render(setting.EmailContentTemplate, variables); sendInputs.Add(new SendNotificationInput { SettingId = setting.Id, NotificationType = NotificationTypeEnum.Email, Title = emailSubject, Content = emailContent, RecipientPersonnelIds = new List { personId.Key }, BusinessType = "WorkOrderPendingSubmit", BusinessId = null, BusinessData = "", SendImmediately = true }); } } int sent = 0; foreach (var input in sendInputs) { var result = await _notificationService.SendNotificationAsync(input); sent += result.SuccessCount; } return new OkObjectResult(new { success = true, sent, targeted = intersectRecipients.Count }); } /// /// 下周计划与请假提醒 /// /// [HttpGet] [AllowAnonymous] public async Task CheckNextWeekPlanAndLeaveReminder() { var setting = await GetNotificationSettingCache("下周计划与请假提醒"); if (setting == null || !setting.IsEnabled) { _logger.LogWarning("下周计划与请假提醒未查询到有效的通知配置或已禁用"); return new OkObjectResult(new { success = true, sent = 0, skipped = 0 }); } var now = DateTime.Now; if (!IsWithinTimeWindow(setting, now)) { _logger.LogInformation("当前不在通知时间窗内,跳过发送"); return new OkObjectResult(new { success = true, sent = 0, skipped = 0 }); } var recipients = await GetNotificationPersonListAsync(setting); if (recipients == null || recipients.Count == 0) { _logger.LogInformation("人员组无成员,跳过发送"); return new OkObjectResult(new { success = true, sent = 0, skipped = 0 }); } var nextWeekStart = now.Date.AddDays(7 - (int)now.DayOfWeek + 1); var nextWeekEnd = nextWeekStart.AddDays(6); var variables = new Dictionary { ["StartDate"] = nextWeekStart.ToString("yyyy-MM-dd"), ["EndDate"] = nextWeekEnd.ToString("yyyy-MM-dd"), ["Now"] = now.ToString("yyyy-MM-dd HH:mm") }; var sysTitle = string.IsNullOrWhiteSpace(setting.SystemMessageTitleTemplate) ? "下周计划与请假提醒" : await Render(setting.SystemMessageTitleTemplate, variables); var sysContent = string.IsNullOrWhiteSpace(setting.SystemMessageContentTemplate) ? $"请提交下周({variables["StartDate"]} ~ {variables["EndDate"]})的工作计划与请假安排" : await Render(setting.SystemMessageContentTemplate, variables); var emailSubject = string.IsNullOrWhiteSpace(setting.EmailSubjectTemplate) ? sysTitle : await Render(setting.EmailSubjectTemplate, variables); var emailContent = string.IsNullOrWhiteSpace(setting.EmailContentTemplate) ? sysContent : await Render(setting.EmailContentTemplate, variables); // 收集需发送目标并按类型合并为批量收件人,减少发送次数 var systemTargets = new List(); var emailTargets = new List(); foreach (var pid in recipients) { var lastSentAt = await GetLastSentAt(setting.Id, pid, "NextWeekPlanAndLeave"); if (!ShouldResendForPerson(setting, now, lastSentAt)) { continue; } if (setting.IsSystemMessageEnabled) { systemTargets.Add(pid); } if (setting.IsEmailEnabled) { emailTargets.Add(pid); } } var sendInputs = new List(); if (setting.IsSystemMessageEnabled && systemTargets.Count > 0) { sendInputs.Add(new SendNotificationInput { SettingId = setting.Id, NotificationType = NotificationTypeEnum.SystemMessage, Title = sysTitle, Content = sysContent, RecipientPersonnelIds = systemTargets.Distinct().ToList(), BusinessType = "NextWeekPlanAndLeave", BusinessId = null, BusinessData = "", SendImmediately = true }); } if (setting.IsEmailEnabled && emailTargets.Count > 0) { sendInputs.Add(new SendNotificationInput { SettingId = setting.Id, NotificationType = NotificationTypeEnum.Email, Title = emailSubject, Content = emailContent, RecipientPersonnelIds = emailTargets.Distinct().ToList(), BusinessType = "NextWeekPlanAndLeave", BusinessId = null, BusinessData = "", SendImmediately = true }); } int sent = 0; foreach (var input in sendInputs) { var result = await _notificationService.SendGroupNotificationAsync(input); sent += result.SuccessCount; } // targeted 返回真实被筛选后的人数 var targeted = systemTargets.Union(emailTargets).Distinct().Count(); return new OkObjectResult(new { success = true, sent, targeted }); } /// /// 任务分配提醒(周五提醒排班管理员进行排班) /// /// [HttpGet] [AllowAnonymous] public async Task CheckFridayScheduleAssignmentReminder() { // 1. 读取通知配置 var setting = await GetNotificationSettingCache("任务分配提醒-周五排班"); if (setting == null || !setting.IsEnabled) { _logger.LogWarning("任务分配提醒-周五排班 未查询到有效的通知配置或已禁用"); return new OkObjectResult(new { success = true, sent = 0, skipped = 0 }); } // 2. 基于时间窗与星期校验(建议由调度保证周五触发,此处再保护一次) var now = DateTime.Now; if (now.DayOfWeek != DayOfWeek.Friday) { _logger.LogInformation("今天不是周五,跳过发送"); return new OkObjectResult(new { success = true, sent = 0, skipped = 0 }); } if (!IsWithinTimeWindow(setting, now)) { _logger.LogInformation("当前不在通知时间窗内,跳过发送"); return new OkObjectResult(new { success = true, sent = 0, skipped = 0 }); } // 3. 获取人员组成员(排班管理员) var recipients = await GetNotificationPersonListAsync(setting); if (recipients == null || recipients.Count == 0) { _logger.LogInformation("人员组无成员,跳过发送"); return new OkObjectResult(new { success = true, sent = 0, skipped = 0 }); } // 4. 变量渲染 var fridayDate = now.Date.ToString("yyyy-MM-dd"); var weekOfYear = System.Globalization.ISOWeek.GetWeekOfYear(now); var variables = new Dictionary { ["Now"] = now.ToString("yyyy-MM-dd HH:mm"), ["FridayDate"] = fridayDate, ["WeekOfYear"] = weekOfYear.ToString() }; var sysTitle = string.IsNullOrWhiteSpace(setting.SystemMessageTitleTemplate) ? "任务分配提醒" : await Render(setting.SystemMessageTitleTemplate, variables); var sysContent = string.IsNullOrWhiteSpace(setting.SystemMessageContentTemplate) ? $"今天是周五({fridayDate}),请及时完成下周的排班任务分配。" : await Render(setting.SystemMessageContentTemplate, variables); var emailSubject = string.IsNullOrWhiteSpace(setting.EmailSubjectTemplate) ? sysTitle : await Render(setting.EmailSubjectTemplate, variables); var emailContent = string.IsNullOrWhiteSpace(setting.EmailContentTemplate) ? sysContent : await Render(setting.EmailContentTemplate, variables); // 5. 频率控制 + 组发 var systemTargets = new List(); var emailTargets = new List(); foreach (var pid in recipients) { var lastSentAt = await GetLastSentAt(setting.Id, pid, "FridayScheduleAssignmentReminder"); if (!ShouldResendForPerson(setting, now, lastSentAt)) { continue; } if (setting.IsSystemMessageEnabled) systemTargets.Add(pid); if (setting.IsEmailEnabled) emailTargets.Add(pid); } var sendInputs = new List(); if (setting.IsSystemMessageEnabled && systemTargets.Count > 0) { sendInputs.Add(new SendNotificationInput { SettingId = setting.Id, NotificationType = NotificationTypeEnum.SystemMessage, Title = sysTitle, Content = sysContent, RecipientPersonnelIds = systemTargets.Distinct().ToList(), BusinessType = "FridayScheduleAssignmentReminder", BusinessId = null, BusinessData = "", SendImmediately = true }); } if (setting.IsEmailEnabled && emailTargets.Count > 0) { sendInputs.Add(new SendNotificationInput { SettingId = setting.Id, NotificationType = NotificationTypeEnum.Email, Title = emailSubject, Content = emailContent, RecipientPersonnelIds = emailTargets.Distinct().ToList(), BusinessType = "FridayScheduleAssignmentReminder", BusinessId = null, BusinessData = "", SendImmediately = true }); } // 6. 发送 int sent = 0; foreach (var input in sendInputs) { // 与“下周计划与请假提醒”保持一致,组发 var result = await _notificationService.SendGroupNotificationAsync(input); sent += result.SuccessCount; } var targeted = systemTargets.Union(emailTargets).Distinct().Count(); return new OkObjectResult(new { success = true, sent, targeted }); } /// /// 人员资质到期预警 /// /// [HttpGet] [AllowAnonymous] public async Task CheckPersonnelQualificationExpiryReminder() { var setting = await GetNotificationSettingCache("人员资质到期预警"); if (setting == null || !setting.IsEnabled) { _logger.LogWarning("人员资质到期预警未查询到有效的通知配置或已禁用"); return new OkObjectResult(new { success = true, sent = 0, skipped = 0 }); } var now = DateTime.Now; if (!IsWithinTimeWindow(setting, now)) { _logger.LogInformation("当前不在通知时间窗内,跳过发送"); return new OkObjectResult(new { success = true, sent = 0, skipped = 0 }); } // // 人员范围:通知配置的人员组 // var recipients = await GetNotificationPersonListAsync(setting); // if (recipients == null || recipients.Count == 0) // { // _logger.LogInformation("人员组无成员,跳过发送"); // return new OkObjectResult(new { success = true, sent = 0, skipped = 0 }); // } // 查询这些人员的预警期内即将到期的资质 var expiring = await _personnelQualificationService.GetExpiringAsync(DateTime.Today, null, includeExpired: false); if (expiring == null || expiring.Count == 0) { _logger.LogInformation("无资质到期预警数据,跳过发送"); return new OkObjectResult(new { success = true, sent = 0, targeted = 0 }); } // 按人员聚合 var grouped = expiring.GroupBy(x => x.PersonnelId) .ToDictionary(g => g.Key, g => g.ToList()); var sendInputs = new List(); int targeted = 0; foreach (var pid in grouped.Distinct()) { if (!grouped.TryGetValue(pid.Key, out var items) || items.Count == 0) continue; var lastSentAt = await GetLastSentAt(setting.Id, pid.Key, "PersonnelQualificationExpiry"); if (!ShouldResendForPerson(setting, now, lastSentAt)) continue; targeted++; // 组装模板变量 var personName = items.FirstOrDefault()?.PersonnelName ?? ""; var count = items.Count; var itemLines = items .OrderBy(i => i.DaysLeft) .Select(i => { var d = i.ExpiryDate?.ToString("yyyy-MM-dd") ?? "-"; return $"{i.QualificationName} 将于 {d} 到期,剩余 {i.DaysLeft} 天"; }); var itemsText = string.Join("; ", itemLines); var variables = new Dictionary { ["UserId"] = pid.ToString(), ["PersonnelName"] = personName, ["Count"] = count.ToString(), ["Items"] = itemsText, ["Now"] = now.ToString("yyyy-MM-dd HH:mm") }; var sysTitle = string.IsNullOrWhiteSpace(setting.SystemMessageTitleTemplate) ? "人员资质到期预警" : await Render(setting.SystemMessageTitleTemplate, variables); var sysContent = string.IsNullOrWhiteSpace(setting.SystemMessageContentTemplate) ? $"您有{count}条资质即将到期:{itemsText}" : await Render(setting.SystemMessageContentTemplate, variables); if (setting.IsSystemMessageEnabled) { sendInputs.Add(new SendNotificationInput { SettingId = setting.Id, NotificationType = NotificationTypeEnum.SystemMessage, Title = sysTitle, Content = sysContent, RecipientPersonnelIds = new List { pid.Key }, BusinessType = "PersonnelQualificationExpiry", BusinessId = null, BusinessData = "", SendImmediately = true }); } if (setting.IsEmailEnabled) { var emailSubject = string.IsNullOrWhiteSpace(setting.EmailSubjectTemplate) ? sysTitle : await Render(setting.EmailSubjectTemplate, variables); var emailContent = string.IsNullOrWhiteSpace(setting.EmailContentTemplate) ? sysContent : await Render(setting.EmailContentTemplate, variables); sendInputs.Add(new SendNotificationInput { SettingId = setting.Id, NotificationType = NotificationTypeEnum.Email, Title = emailSubject, Content = emailContent, RecipientPersonnelIds = new List { pid.Key }, BusinessType = "PersonnelQualificationExpiry", BusinessId = null, BusinessData = "", SendImmediately = true }); } } int sent = 0; foreach (var input in sendInputs) { var result = await _notificationService.SendNotificationAsync(input); sent += result.SuccessCount; } return new OkObjectResult(new { success = true, sent, targeted }); } /// /// 统一对外:工作任务操作通知(供 WorkOrderIntegrationOperationService 调用) /// /// Modify/Delete/Cancel /// 任务ID /// 操作者用户ID(可空) /// 操作者名称(可空) /// 备注(可空) /// [HttpPost] [AllowAnonymous] public async Task SendWorkOrderOperationNotificationAsync( string operationType, long taskId, long? operatorUserId = null, string operatorName = null, string remarks = null, IList designatedPersonnelIds = null, IList designatedPersonnelNames = null) { // 1. 读取统一通知配置 var setting = await GetNotificationSettingCache("工作任务操作通知"); if (setting == null || !setting.IsEnabled) { _logger.LogWarning("工作任务操作通知 未查询到有效的通知配置或已禁用"); return new OkObjectResult(new { success = true, sent = 0, skipped = 0 }); } var now = DateTime.Now; if (!IsWithinTimeWindow(setting, now)) { _logger.LogInformation("当前不在通知时间窗内,跳过发送"); return new OkObjectResult(new { success = true, sent = 0, skipped = 0 }); } // 2. 收件人 var recipients = await GetNotificationPersonListAsync(setting); recipients.AddRange(designatedPersonnelIds); recipients = recipients.Distinct().ToList(); if (recipients == null || recipients.Count == 0) { _logger.LogInformation("人员组无成员,跳过发送"); return new OkObjectResult(new { success = true, sent = 0, skipped = 0 }); } // 3. 业务类型区分,便于历史频率控制 string businessType = operationType?.ToLower() switch { "modify" => "WorkOrderModified", "delete" => "WorkOrderDeleted", "cancel" => "WorkOrderCancelled", _ => "WorkOrderOperation" }; // 4. 模板变量 var variables = new Dictionary { ["OperationType"] = operationType ?? string.Empty, ["TaskId"] = taskId.ToString(), ["OperatorUserId"] = operatorUserId?.ToString() ?? string.Empty, ["OperatorName"] = operatorName ?? string.Empty, ["Remarks"] = remarks ?? string.Empty, ["Now"] = now.ToString("yyyy-MM-dd HH:mm"), ["DesignatedPersonnelIds"] = designatedPersonnelIds != null ? string.Join(",", designatedPersonnelIds) : string.Empty, ["DesignatedPersonnelNames"] = designatedPersonnelNames != null ? string.Join(",", designatedPersonnelNames) : string.Empty }; var sysTitle = string.IsNullOrWhiteSpace(setting.SystemMessageTitleTemplate) ? $"任务{operationType}通知" : await Render(setting.SystemMessageTitleTemplate, variables); var sysContent = string.IsNullOrWhiteSpace(setting.SystemMessageContentTemplate) ? $"任务({taskId})已执行{operationType}操作。{(string.IsNullOrEmpty(remarks) ? "" : "备注:" + remarks)}" : await Render(setting.SystemMessageContentTemplate, variables); var emailSubject = string.IsNullOrWhiteSpace(setting.EmailSubjectTemplate) ? sysTitle : await Render(setting.EmailSubjectTemplate, variables); var emailContent = string.IsNullOrWhiteSpace(setting.EmailContentTemplate) ? sysContent : await Render(setting.EmailContentTemplate, variables); // 5. 频率控制 + 组发 var systemTargets = new List(); var emailTargets = new List(); foreach (var pid in recipients) { var lastSentAt = await GetLastSentAt(setting.Id, pid, businessType); if (!ShouldResendForPerson(setting, now, lastSentAt)) { continue; } if (setting.IsSystemMessageEnabled) systemTargets.Add(pid); if (setting.IsEmailEnabled) emailTargets.Add(pid); } var sendInputs = new List(); if (setting.IsSystemMessageEnabled && systemTargets.Count > 0) { sendInputs.Add(new SendNotificationInput { SettingId = setting.Id, NotificationType = NotificationTypeEnum.SystemMessage, Title = sysTitle, Content = sysContent, RecipientPersonnelIds = systemTargets.Distinct().ToList(), BusinessType = businessType, BusinessId = taskId, BusinessData = System.Text.Json.JsonSerializer.Serialize(new { OperationType = operationType, TaskId = taskId, OperatorUserId = operatorUserId, OperatorName = operatorName, Remarks = remarks }), SendImmediately = true }); } if (setting.IsEmailEnabled && emailTargets.Count > 0) { sendInputs.Add(new SendNotificationInput { SettingId = setting.Id, NotificationType = NotificationTypeEnum.Email, Title = emailSubject, Content = emailContent, RecipientPersonnelIds = emailTargets.Distinct().ToList(), BusinessType = businessType, BusinessId = taskId, BusinessData = System.Text.Json.JsonSerializer.Serialize(new { OperationType = operationType, TaskId = taskId, OperatorUserId = operatorUserId, OperatorName = operatorName, Remarks = remarks }), SendImmediately = true }); } int sent = 0; foreach (var input in sendInputs) { var result = await _notificationService.SendGroupNotificationAsync(input); sent += result.SuccessCount; } var targeted = systemTargets.Union(emailTargets).Distinct().Count(); return new OkObjectResult(new { success = true, sent, targeted, businessType }); } private async Task Render(string template, Dictionary variables) { var isValid = await _notificationService.ValidateTemplateAsync(template); if (!isValid) return template; if (variables == null || variables.Count == 0) return template; var result = template; foreach (var kv in variables) { var placeholder = $"{{{kv.Key}}}"; result = result.Replace(placeholder, kv.Value ?? string.Empty, StringComparison.Ordinal); } return result; } private async Task GetLastSentAt(long settingId, long personnelId, string businessType) { try { var histories = await _notificationService.GetNotificationHistoryPageAsync(new ZhonTai.Admin.Core.Dto.PageInput { CurrentPage = 0, PageSize = 10000, Filter = new NotificationHistoryPageInput { NotificationSettingId = settingId, RecipientPersonnelId = personnelId, BusinessType = businessType } }); var item = histories.List.FirstOrDefault(); return item?.ActualSendTime ?? item?.PlannedSendTime; } catch { return null; } } private async Task GetNotificationSettingCache(string key) { var settings = await _notificationService.GetNotificationSettingListAsync(true); return settings.FirstOrDefault(m => m.NotificationName == key); } private async Task> GetNotificationPersonListAsync(NotificationSettingOutput? setting = null, List? otherPersons = null) { var notificationPersonList = new List(); if (setting != null) { notificationPersonList.AddRange(await _notificationService.GetPersonnelGroupMembersAsync(setting.PersonnelGroupId)); } if (otherPersons != null && otherPersons.Count > 0) { notificationPersonList.AddRange(otherPersons); } return notificationPersonList.Distinct().ToList(); } private bool ShouldResendForPerson(NotificationSettingOutput setting, DateTime now, DateTime? lastSentAt) { if (!IsWithinTimeWindow(setting, now)) return false; if (setting.FrequencyType == NotificationFrequencyEnum.Once) { return lastSentAt == null; } if (setting.FrequencyType == NotificationFrequencyEnum.FixedInterval) { if (lastSentAt == null) return true; var interval = setting.IntervalMinutes.GetValueOrDefault(0); if (interval <= 0) return false; return now - lastSentAt.Value >= TimeSpan.FromMinutes(interval); } return false; } private bool IsWithinTimeWindow(NotificationSettingOutput setting, DateTime now) { if (string.IsNullOrWhiteSpace(setting.StartTime) || string.IsNullOrWhiteSpace(setting.EndTime)) return true; if (!TimeOnly.TryParse(setting.StartTime, out var start)) return true; if (!TimeOnly.TryParse(setting.EndTime, out var end)) return true; var t = TimeOnly.FromDateTime(now); return t >= start && t < end; } }