From 2b3f9acdce96bfa8be7e2cdb15dd399be53e969d Mon Sep 17 00:00:00 2001 From: "Asoka.Wang" Date: Mon, 22 Sep 2025 19:09:47 +0800 Subject: [PATCH] 123 --- .../Core/Configuration/EmailConfiguration.cs | 63 + .../Core/Enums/SystemMessageTypeEnum.cs | 32 + .../Core/Enums/WorkOrderStatusEnum.cs | 7 +- .../INotificationHistoryRepository.cs | 66 + .../INotificationSettingRepository.cs | 46 + .../Notification/IPersonnelGroupRepository.cs | 49 + .../PersonnelQualificationStatistics.cs | 43 + .../NPP.SmartSchedue.Api.Contracts.csproj | 1 + .../Integration/IIntegrationRecordService.cs | 14 +- .../ISmartScheduleOrchestratorService.cs | 11 +- .../IWorkOrderIntegrationOperationService.cs | 23 + .../Input/BatchWorkOrderOperationInput.cs | 61 + .../Input/WorkOrderCancelOperationInput.cs | 28 + .../Input/WorkOrderDeleteOperationInput.cs | 33 + .../Input/WorkOrderModifyOperationInput.cs | 25 + .../Models/WorkOrderValidationResult.cs | 34 + .../Output/BatchWorkOrderOperationResult.cs | 71 + .../Output/CancelIntegrationRecordResult.cs | 2 +- .../Output/ProcessTaskChangesResult.cs | 2 +- .../Output/PublishIntegrationRecordResult.cs | 6 +- .../Output/SmartReallocationResult.cs | 2 +- .../Output/VersionManagementResults.cs | 2 +- .../Output/WorkOrderOperationResult.cs | 35 + .../Notification/IEmailNotificationService.cs | 121 +- .../Notification/INotificationService.cs | 46 +- .../INotificationTemplateService.cs | 194 +- .../Notification/ISystemMessageService.cs | 201 +- .../Notification/Input/BatchSendEmailInput.cs | 35 + .../Input/BatchSendPersonalizedEmailInput.cs | 53 + .../Input/BatchSendSystemMessageInput.cs | 47 + .../Input/ExtractVariablesInput.cs | 15 + .../Input/GetBusinessVariablesInput.cs | 25 + .../Input/GetPredefinedTemplateInput.cs | 15 + .../Input/GetPredefinedTemplatesInput.cs | 12 + .../Input/NotificationHistoryPageInput.cs | 47 + .../Input/NotificationSettingPageInput.cs | 10 + .../Input/PersonnelGroupGetPageInput.cs | 18 + .../Input/PersonnelGroupUpdateInput.cs | 16 + .../Notification/Input/RenderTemplateInput.cs | 21 + .../Input/SendEmailByTemplateInput.cs | 39 + .../Notification/Input/SendEmailInput.cs | 40 + .../Input/SendNotificationInput.cs | 5 +- .../Input/SendSystemMessageByTemplateInput.cs | 53 + .../Input/SendSystemMessageInput.cs | 49 + .../SendSystemMessageWithActionsInput.cs | 54 + .../Input/ValidateTemplateInput.cs | 15 + .../Output/BatchSendEmailOutput.cs | 45 + .../Output/BatchSendSystemMessageOutput.cs | 64 + .../Services/Notification/Output/EmailItem.cs | 39 + .../Output/EmailServerStatusOutput.cs | 39 + .../Output/ExtractVariablesOutput.cs | 24 + .../Output/GetBusinessVariablesOutput.cs | 24 + .../Output/GetPredefinedTemplateOutput.cs | 22 + .../Output/GetPredefinedTemplatesOutput.cs | 29 + .../Output/GetSystemVariablesOutput.cs | 24 + .../Notification/Output/PredefinedTemplate.cs | 54 + .../Output/RenderTemplateOutput.cs | 22 + .../Notification/Output/SendEmailOutput.cs | 34 + .../Output/SendSystemMessageOutput.cs | 61 + .../Output/SystemMessageAction.cs | 32 + .../Notification/Output/SystemMessageItem.cs | 50 + .../Output/SystemMessageStatusOutput.cs | 64 + .../Output/TemplateValidationResult.cs | 48 + .../Notification/Output/TemplateVariable.cs | 37 + .../Output/ValidateTemplateOutput.cs | 24 + .../IPersonnelQualificationService.cs | 17 + .../Output/ExpiringQualificationItemOutput.cs | 20 + .../Services/Work/IWorkOrderService.cs | 10 + .../Services/Workbench/IWorkbenchService.cs | 20 + .../Workbench/Input/WeeklyCalendarInput.cs | 31 + .../Workbench/Output/WeeklyCalendarOutput.cs | 199 ++ NPP.SmartSchedue.Api/Core/Consts/CacheKeys.cs | 24 + .../NPP.SmartSchedue.Api.csproj | 4 +- .../NotificationHistoryRepository.cs | 12 +- .../NotificationSettingRepository.cs | 45 +- .../Notification/PersonnelGroupRepository.cs | 1 - .../ServiceCollectionExtensions.cs | 38 + .../Algorithms/ContextBuilderEngine.cs | 16 +- .../Algorithms/LinearProgrammingEngine.cs | 89 +- .../Integration/IntegrationRecordService.cs | 237 ++- .../SmartScheduleOrchestratorService.cs | 11 + .../TaskAssignmentModificationService.cs | 1123 ++++++++++ .../WorkOrderIntegrationOperationService.cs | 987 +++++++++ .../Integration/WorkOrderStatusValidator.cs | 170 ++ .../Notification/EmailNotificationService.cs | 509 +++-- .../NotificationScheduledService.cs | 793 ++++++++ .../Notification/NotificationService.cs | 1805 +++++++++++++++++ .../NotificationTemplateService.cs | 360 +++- .../Notification/SystemMessageService.cs | 476 ++--- .../PersonnelQualificationService.cs | 129 +- .../Services/Work/WorkOrderService.cs | 16 +- .../Services/Workbench/WorkbenchService.cs | 256 +++ .../.config/dotnet-tools.json | 13 + .../ConfigCenter/appconfig.json | 4 +- .../ConfigCenter/dbconfig.json | 2 +- NPP.SmartSchedue.Host/Program.cs | 64 +- .../Properties/launchSettings.json | 4 +- NPP.SmartSchedue.Host/appsettings.json | 30 +- .../NPP.SmartSchedue.Tests.csproj | 1 + WorkOrderAssignmentService_重构总结.md | 170 -- 100 files changed, 8837 insertions(+), 1372 deletions(-) create mode 100644 NPP.SmartSchedue.Api.Contracts/Core/Configuration/EmailConfiguration.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Core/Enums/SystemMessageTypeEnum.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Domain/Notification/INotificationHistoryRepository.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Domain/Notification/INotificationSettingRepository.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Domain/Notification/IPersonnelGroupRepository.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Domain/Personnel/PersonnelQualificationStatistics.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Integration/IWorkOrderIntegrationOperationService.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Integration/Input/BatchWorkOrderOperationInput.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Integration/Input/WorkOrderCancelOperationInput.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Integration/Input/WorkOrderDeleteOperationInput.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Integration/Input/WorkOrderModifyOperationInput.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Integration/Models/WorkOrderValidationResult.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Integration/Output/BatchWorkOrderOperationResult.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Integration/Output/WorkOrderOperationResult.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/BatchSendEmailInput.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/BatchSendPersonalizedEmailInput.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/BatchSendSystemMessageInput.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/ExtractVariablesInput.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/GetBusinessVariablesInput.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/GetPredefinedTemplateInput.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/GetPredefinedTemplatesInput.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/NotificationHistoryPageInput.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/NotificationSettingPageInput.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/PersonnelGroupGetPageInput.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/PersonnelGroupUpdateInput.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/RenderTemplateInput.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/SendEmailByTemplateInput.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/SendEmailInput.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/SendSystemMessageByTemplateInput.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/SendSystemMessageInput.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/SendSystemMessageWithActionsInput.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/ValidateTemplateInput.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/BatchSendEmailOutput.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/BatchSendSystemMessageOutput.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/EmailItem.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/EmailServerStatusOutput.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/ExtractVariablesOutput.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/GetBusinessVariablesOutput.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/GetPredefinedTemplateOutput.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/GetPredefinedTemplatesOutput.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/GetSystemVariablesOutput.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/PredefinedTemplate.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/RenderTemplateOutput.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/SendEmailOutput.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/SendSystemMessageOutput.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/SystemMessageAction.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/SystemMessageItem.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/SystemMessageStatusOutput.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/TemplateValidationResult.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/TemplateVariable.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/ValidateTemplateOutput.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Personnel/Output/ExpiringQualificationItemOutput.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Workbench/IWorkbenchService.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Workbench/Input/WeeklyCalendarInput.cs create mode 100644 NPP.SmartSchedue.Api.Contracts/Services/Workbench/Output/WeeklyCalendarOutput.cs create mode 100644 NPP.SmartSchedue.Api/Services/Integration/TaskAssignmentModificationService.cs create mode 100644 NPP.SmartSchedue.Api/Services/Integration/WorkOrderIntegrationOperationService.cs create mode 100644 NPP.SmartSchedue.Api/Services/Integration/WorkOrderStatusValidator.cs create mode 100644 NPP.SmartSchedue.Api/Services/Notification/NotificationScheduledService.cs create mode 100644 NPP.SmartSchedue.Api/Services/Notification/NotificationService.cs create mode 100644 NPP.SmartSchedue.Api/Services/Workbench/WorkbenchService.cs create mode 100644 NPP.SmartSchedue.Host/.config/dotnet-tools.json delete mode 100644 WorkOrderAssignmentService_重构总结.md diff --git a/NPP.SmartSchedue.Api.Contracts/Core/Configuration/EmailConfiguration.cs b/NPP.SmartSchedue.Api.Contracts/Core/Configuration/EmailConfiguration.cs new file mode 100644 index 0000000..cbf6a7b --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Core/Configuration/EmailConfiguration.cs @@ -0,0 +1,63 @@ +namespace NPP.SmartSchedue.Api.Contracts.Core.Configuration; + +/// +/// 邮件配置 +/// +public class EmailConfiguration +{ + /// + /// 配置节点名称 + /// + public const string SectionName = "EmailNotification"; + + /// + /// SMTP服务器地址 + /// + public string SmtpServer { get; set; } = ""; + + /// + /// SMTP端口 + /// + public int SmtpPort { get; set; } = 587; + + /// + /// 发送者邮箱 + /// + public string SenderEmail { get; set; } = ""; + + /// + /// 发送者密码 + /// + public string SenderPassword { get; set; } = ""; + + /// + /// 发送者名称 + /// + public string SenderName { get; set; } = "NPP智能生产调度系统"; + + /// + /// 是否启用SSL + /// + public bool EnableSsl { get; set; } = true; + + /// + /// 超时时间(秒) + /// + public int TimeoutSeconds { get; set; } = 30; + + /// + /// 是否启用邮件服务 + /// + public bool Enabled { get; set; } = true; + + /// + /// 验证配置是否有效 + /// + public bool IsValid() + { + return !string.IsNullOrWhiteSpace(SmtpServer) && + SmtpPort > 0 && + !string.IsNullOrWhiteSpace(SenderEmail) && + !string.IsNullOrWhiteSpace(SenderPassword); + } +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Core/Enums/SystemMessageTypeEnum.cs b/NPP.SmartSchedue.Api.Contracts/Core/Enums/SystemMessageTypeEnum.cs new file mode 100644 index 0000000..424aa5b --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Core/Enums/SystemMessageTypeEnum.cs @@ -0,0 +1,32 @@ +namespace NPP.SmartSchedue.Api.Contracts.Core.Enums; + +/// +/// 系统消息类型枚举 +/// +public enum SystemMessageTypeEnum +{ + /// + /// 信息 + /// + Info = 1, + + /// + /// 成功 + /// + Success = 2, + + /// + /// 警告 + /// + Warning = 3, + + /// + /// 错误 + /// + Error = 4, + + /// + /// 紧急 + /// + Urgent = 5 +} diff --git a/NPP.SmartSchedue.Api.Contracts/Core/Enums/WorkOrderStatusEnum.cs b/NPP.SmartSchedue.Api.Contracts/Core/Enums/WorkOrderStatusEnum.cs index 61e6697..4bade48 100644 --- a/NPP.SmartSchedue.Api.Contracts/Core/Enums/WorkOrderStatusEnum.cs +++ b/NPP.SmartSchedue.Api.Contracts/Core/Enums/WorkOrderStatusEnum.cs @@ -38,5 +38,10 @@ public enum WorkOrderStatusEnum /// /// 已完成 /// - Completed = 7 + Completed = 7, + + /// + /// 已取消 + /// + Cancelled = 99 } \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Domain/Notification/INotificationHistoryRepository.cs b/NPP.SmartSchedue.Api.Contracts/Domain/Notification/INotificationHistoryRepository.cs new file mode 100644 index 0000000..e34af80 --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Domain/Notification/INotificationHistoryRepository.cs @@ -0,0 +1,66 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using NPP.SmartSchedue.Api.Contracts.Core.Enums; +using ZhonTai.Admin.Core.Repositories; + +namespace NPP.SmartSchedue.Api.Contracts.Domain.Notification; + +/// +/// 通知历史记录仓储接口 +/// +public interface INotificationHistoryRepository : IRepositoryBase +{ + /// + /// 根据通知设置ID获取历史记录列表 + /// + Task> GetByNotificationSettingIdAsync(long notificationSettingId); + + /// + /// 根据接收人员ID获取历史记录列表 + /// + Task> GetByRecipientPersonnelIdAsync(long recipientPersonnelId); + + /// + /// 根据发送状态获取历史记录列表 + /// + Task> GetBySendStatusAsync(NotificationStatusEnum sendStatus); + + /// + /// 根据通知方式获取历史记录列表 + /// + Task> GetByNotificationTypeAsync(NotificationTypeEnum notificationType); + + /// + /// 根据业务类型和业务ID获取历史记录列表 + /// + Task> GetByBusinessAsync(string businessType, long? businessId = null); + + /// + /// 获取需要重试的失败通知列表 + /// + Task> GetFailedNotificationsForRetryAsync(int? maxRetryCount = null); + + /// + /// 获取指定时间范围内的通知统计信息 + /// + Task> GetNotificationStatisticsAsync( + DateTime startTime, + DateTime endTime, + long? notificationSettingId = null); + + /// + /// 根据日期范围获取历史记录列表 + /// + Task> GetByDateRangeAsync(DateTime startDate, DateTime endDate); + + /// + /// 更新通知发送状态 + /// + Task UpdateSendStatusAsync(long id, NotificationStatusEnum sendStatus, string sendResult = "", string errorMessage = ""); + + /// + /// 批量更新通知发送状态 + /// + Task BatchUpdateSendStatusAsync(List<(long Id, NotificationStatusEnum Status, string Result, string Error)> updates); +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Domain/Notification/INotificationSettingRepository.cs b/NPP.SmartSchedue.Api.Contracts/Domain/Notification/INotificationSettingRepository.cs new file mode 100644 index 0000000..6c2d7a0 --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Domain/Notification/INotificationSettingRepository.cs @@ -0,0 +1,46 @@ +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using ZhonTai.Admin.Core.Repositories; + +namespace NPP.SmartSchedue.Api.Contracts.Domain.Notification; + +/// +/// 通知设置仓储接口 +/// +public interface INotificationSettingRepository : IRepositoryBase +{ + /// + /// 根据启用状态获取通知设置列表 + /// + Task> GetByEnabledAsync(bool enabled); + + /// + /// 根据人员组ID获取通知设置列表 + /// + Task> GetByPersonnelGroupIdAsync(long personnelGroupId); + + /// + /// 根据通知方式获取通知设置列表 + /// + Task> GetByNotificationTypeAsync(int notificationType); + + /// + /// 根据触发条件获取匹配的通知设置列表 + /// 这里实现一个简化的匹配逻辑,实际项目中可能需要更复杂的条件匹配引擎 + /// + Task> GetMatchingNotificationSettingsAsync( + string businessType, + Dictionary businessContext); + + /// + /// 检查通知设置名称是否存在 + /// + Task ExistsNotificationNameAsync(string notificationName, long? excludeId = null); + + /// + /// 获取需要在当前时间执行的通知设置列表 + /// 根据决策点2:简单时间段,只支持开始时间-结束时间 + /// + Task> GetActiveNotificationSettingsForTimeAsync(DateTime currentTime); +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Domain/Notification/IPersonnelGroupRepository.cs b/NPP.SmartSchedue.Api.Contracts/Domain/Notification/IPersonnelGroupRepository.cs new file mode 100644 index 0000000..2285067 --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Domain/Notification/IPersonnelGroupRepository.cs @@ -0,0 +1,49 @@ +using System.Collections.Generic; +using System.Threading.Tasks; +using NPP.SmartSchedue.Api.Contracts.Core.Enums; +using ZhonTai.Admin.Core.Repositories; + +namespace NPP.SmartSchedue.Api.Contracts.Domain.Notification; + +/// +/// 人员组仓储接口 +/// +public interface IPersonnelGroupRepository : IRepositoryBase +{ + /// + /// 根据启用状态获取人员组列表 + /// + Task> GetByEnabledAsync(bool enabled); + + /// + /// 根据人员组类型获取人员组列表 + /// + Task> GetByGroupTypeAsync(PersonnelGroupTypeEnum groupType); + + /// + /// 检查人员组名称是否存在 + /// + Task ExistsGroupNameAsync(string groupName, long? excludeId = null); + + /// + /// 获取包含指定人员的人员组列表 + /// + Task> GetGroupsContainingPersonnelAsync(long personnelId); + + /// + /// 获取包含指定部门的人员组列表 + /// + Task> GetGroupsContainingDepartmentAsync(long departmentId); + + /// + /// 获取包含指定职位的人员组列表 + /// + Task> GetGroupsContainingPositionAsync(string position); + + /// + /// 计算人员组的实际人员数量 + /// 这个方法返回一个估算值,实际计算需要在Service层进行 + /// 因为需要查询人员表和考虑动态规则 + /// + Task CalculatePersonnelCountAsync(long personnelGroupId); +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Domain/Personnel/PersonnelQualificationStatistics.cs b/NPP.SmartSchedue.Api.Contracts/Domain/Personnel/PersonnelQualificationStatistics.cs new file mode 100644 index 0000000..3791790 --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Domain/Personnel/PersonnelQualificationStatistics.cs @@ -0,0 +1,43 @@ +using System; +using System.Collections.Generic; + +namespace NPP.SmartSchedue.Api.Contracts.Domain.Personnel +{ + /// + /// 人员资质统计信息 + /// + public class PersonnelQualificationStatistics + { + /// + /// 资质ID + /// + public long QualificationId { get; set; } + + /// + /// 资质名称 + /// + public string QualificationName { get; set; } + + /// + /// 持有该资质的人员数量 + /// + public int PersonnelCount { get; set; } + } + + /// + /// 人员资质统计结果 + /// + public class PersonnelQualificationStatisticsResult + { + /// + /// 统计数据列表 + /// 横坐标为资质,纵坐标为人员数量 + /// + public List Statistics { get; set; } = new List(); + + /// + /// 统计时间 + /// + public DateTime GeneratedTime { get; set; } = DateTime.Now; + } +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/NPP.SmartSchedue.Api.Contracts.csproj b/NPP.SmartSchedue.Api.Contracts/NPP.SmartSchedue.Api.Contracts.csproj index efdd3c3..17b060a 100644 --- a/NPP.SmartSchedue.Api.Contracts/NPP.SmartSchedue.Api.Contracts.csproj +++ b/NPP.SmartSchedue.Api.Contracts/NPP.SmartSchedue.Api.Contracts.csproj @@ -35,6 +35,7 @@ + diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Integration/IIntegrationRecordService.cs b/NPP.SmartSchedue.Api.Contracts/Services/Integration/IIntegrationRecordService.cs index ffd6309..08027c5 100644 --- a/NPP.SmartSchedue.Api.Contracts/Services/Integration/IIntegrationRecordService.cs +++ b/NPP.SmartSchedue.Api.Contracts/Services/Integration/IIntegrationRecordService.cs @@ -66,6 +66,18 @@ namespace NPP.SmartSchedue.Api.Contracts.Services.Integration #endregion + #region 任务修改接口 + + /// + /// 修改整合记录中的任务分配 + /// 根据选中的人员修改任务的指定人员ID和姓名,并更新整合记录 + /// + /// 任务修改输入参数 + /// 修改结果 + Task ModifyIntegrationRecordTaskAsync(IntegrationRecordTaskModifyInput input); + + #endregion + #region 通知管理接口(预留) /// @@ -77,7 +89,7 @@ namespace NPP.SmartSchedue.Api.Contracts.Services.Integration /// 目标人员ID列表 /// 自定义消息 /// 通知发送结果 - Task SendPublishNotificationAsync(long recordId, string notificationType, List targetPersonnelIds, string customMessage = ""); + Task SendPublishNotificationAsync(long recordId, string notificationType, List targetPersonnelIds, string customMessage = ""); /// /// 创建待办任务(预留接口) diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Integration/ISmartScheduleOrchestratorService.cs b/NPP.SmartSchedue.Api.Contracts/Services/Integration/ISmartScheduleOrchestratorService.cs index f6729a0..735d806 100644 --- a/NPP.SmartSchedue.Api.Contracts/Services/Integration/ISmartScheduleOrchestratorService.cs +++ b/NPP.SmartSchedue.Api.Contracts/Services/Integration/ISmartScheduleOrchestratorService.cs @@ -1,6 +1,15 @@ +using System.Threading.Tasks; +using NPP.SmartSchedue.Api.Contracts.Services.Integration.Input; + namespace NPP.SmartSchedue.Api.Contracts.Services.Integration; public interface ISmartScheduleOrchestratorService { - + /// + /// 修改整合记录中的任务分配 + /// 根据选中的人员修改任务的指定人员ID和姓名,并更新整合记录 + /// + /// 任务修改输入参数 + /// 修改结果 + Task ModifyIntegrationRecordTaskAsync(IntegrationRecordTaskModifyInput input); } \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Integration/IWorkOrderIntegrationOperationService.cs b/NPP.SmartSchedue.Api.Contracts/Services/Integration/IWorkOrderIntegrationOperationService.cs new file mode 100644 index 0000000..1a50b94 --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Integration/IWorkOrderIntegrationOperationService.cs @@ -0,0 +1,23 @@ +using NPP.SmartSchedue.Api.Contracts.Services.Integration.Input; +using NPP.SmartSchedue.Api.Contracts.Services.Integration.Output; +using System.Threading.Tasks; + +namespace NPP.SmartSchedue.Api.Contracts.Services.Integration +{ + /// + /// 工作订单整合操作统一服务 + /// 简化版:专注核心业务流程,去除复杂的权限、版本、审计功能 + /// + public interface IWorkOrderIntegrationOperationService + { + /// + /// 修改任务 + /// + Task ModifyWorkOrderAsync(WorkOrderModifyOperationInput input); + + /// + /// 删除任务 + /// + Task DeleteWorkOrderAsync(WorkOrderDeleteOperationInput input); + } +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Integration/Input/BatchWorkOrderOperationInput.cs b/NPP.SmartSchedue.Api.Contracts/Services/Integration/Input/BatchWorkOrderOperationInput.cs new file mode 100644 index 0000000..525ea53 --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Integration/Input/BatchWorkOrderOperationInput.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; + +namespace NPP.SmartSchedue.Api.Contracts.Services.Integration.Input +{ + /// + /// 批量操作类型 + /// + public enum BatchOperationType + { + /// + /// 批量修改 + /// + BatchModify = 1, + + /// + /// 批量删除 + /// + BatchDelete = 2, + + /// + /// 批量取消 + /// + BatchCancel = 3 + } + + /// + /// 批量任务操作输入 + /// + public class BatchWorkOrderOperationInput + { + /// + /// 操作类型 + /// + public BatchOperationType OperationType { get; set; } + + /// + /// 任务ID列表 + /// + public List TaskIds { get; set; } = new List(); + + /// + /// 操作参数(根据操作类型不同,内容不同) + /// + public object OperationParameters { get; set; } + + /// + /// 操作员用户ID + /// + public long OperatorUserId { get; set; } + + /// + /// 操作员姓名 + /// + public string OperatorName { get; set; } + + /// + /// 操作备注 + /// + public string Remarks { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Integration/Input/WorkOrderCancelOperationInput.cs b/NPP.SmartSchedue.Api.Contracts/Services/Integration/Input/WorkOrderCancelOperationInput.cs new file mode 100644 index 0000000..95fb892 --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Integration/Input/WorkOrderCancelOperationInput.cs @@ -0,0 +1,28 @@ +namespace NPP.SmartSchedue.Api.Contracts.Services.Integration.Input +{ + /// + /// 任务取消操作输入 + /// + public class WorkOrderCancelOperationInput + { + /// + /// 任务ID + /// + public long TaskId { get; set; } + + /// + /// 取消原因 + /// + public string CancelReason { get; set; } = string.Empty; + + /// + /// 操作员用户ID + /// + public long OperatorUserId { get; set; } + + /// + /// 操作员姓名 + /// + public string OperatorName { get; set; } + } +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Integration/Input/WorkOrderDeleteOperationInput.cs b/NPP.SmartSchedue.Api.Contracts/Services/Integration/Input/WorkOrderDeleteOperationInput.cs new file mode 100644 index 0000000..dcccea3 --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Integration/Input/WorkOrderDeleteOperationInput.cs @@ -0,0 +1,33 @@ +namespace NPP.SmartSchedue.Api.Contracts.Services.Integration.Input +{ + /// + /// 任务删除操作输入 + /// + public class WorkOrderDeleteOperationInput + { + /// + /// 任务ID + /// + public long TaskId { get; set; } + + /// + /// 是否软删除(true=软删除,false=物理删除) + /// + public bool IsSoftDelete { get; set; } = true; + + /// + /// 删除原因 + /// + public string DeleteReason { get; set; } = string.Empty; + + /// + /// 操作员用户ID + /// + public long OperatorUserId { get; set; } + + /// + /// 操作员姓名 + /// + public string OperatorName { get; set; } + } +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Integration/Input/WorkOrderModifyOperationInput.cs b/NPP.SmartSchedue.Api.Contracts/Services/Integration/Input/WorkOrderModifyOperationInput.cs new file mode 100644 index 0000000..bfe3910 --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Integration/Input/WorkOrderModifyOperationInput.cs @@ -0,0 +1,25 @@ +using NPP.SmartSchedue.Api.Contracts.Services.Work.Input; + +namespace NPP.SmartSchedue.Api.Contracts.Services.Integration.Input +{ + /// + /// 任务修改操作输入 + /// + public class WorkOrderModifyOperationInput + { + /// + /// 任务ID + /// + public long TaskId { get; set; } + + /// + /// 修改的任务数据 + /// + public WorkOrderUpdateInput TaskModifyData { get; set; } + + /// + /// 操作备注 + /// + public string Remarks { get; set; } = string.Empty; + } +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Integration/Models/WorkOrderValidationResult.cs b/NPP.SmartSchedue.Api.Contracts/Services/Integration/Models/WorkOrderValidationResult.cs new file mode 100644 index 0000000..e884abc --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Integration/Models/WorkOrderValidationResult.cs @@ -0,0 +1,34 @@ +namespace NPP.SmartSchedue.Api.Contracts.Services.Integration.Models +{ + /// + /// 工作订单验证结果 + /// + public class WorkOrderValidationResult + { + /// + /// 验证是否通过 + /// + public bool IsValid { get; set; } + + /// + /// 错误消息 + /// + public string ErrorMessage { get; set; } = string.Empty; + + /// + /// 创建成功结果 + /// + public static WorkOrderValidationResult Success() + { + return new WorkOrderValidationResult { IsValid = true }; + } + + /// + /// 创建失败结果 + /// + public static WorkOrderValidationResult Fail(string errorMessage) + { + return new WorkOrderValidationResult { IsValid = false, ErrorMessage = errorMessage }; + } + } +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Integration/Output/BatchWorkOrderOperationResult.cs b/NPP.SmartSchedue.Api.Contracts/Services/Integration/Output/BatchWorkOrderOperationResult.cs new file mode 100644 index 0000000..d39d9a3 --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Integration/Output/BatchWorkOrderOperationResult.cs @@ -0,0 +1,71 @@ +using System.Collections.Generic; + +namespace NPP.SmartSchedue.Api.Contracts.Services.Integration.Output +{ + /// + /// 单个任务操作结果 + /// + public class SingleTaskOperationResult + { + /// + /// 任务ID + /// + public long TaskId { get; set; } + + /// + /// 操作是否成功 + /// + public bool IsSuccess { get; set; } + + /// + /// 操作消息 + /// + public string Message { get; set; } = string.Empty; + + /// + /// 错误详情 + /// + public List ErrorDetails { get; set; } = new List(); + } + + /// + /// 批量操作结果输出 + /// + public class BatchWorkOrderOperationResult + { + /// + /// 总体操作是否成功 + /// + public bool IsSuccess { get; set; } + + /// + /// 总体操作消息 + /// + public string Message { get; set; } = string.Empty; + + /// + /// 总任务数 + /// + public int TotalCount { get; set; } + + /// + /// 成功任务数 + /// + public int SuccessCount { get; set; } + + /// + /// 失败任务数 + /// + public int FailedCount { get; set; } + + /// + /// 每个任务的操作结果 + /// + public List TaskResults { get; set; } = new List(); + + /// + /// 影响的整合记录ID列表 + /// + public List AffectedIntegrationRecordIds { get; set; } = new List(); + } +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Integration/Output/CancelIntegrationRecordResult.cs b/NPP.SmartSchedue.Api.Contracts/Services/Integration/Output/CancelIntegrationRecordResult.cs index 21d9fbf..3b7e7bf 100644 --- a/NPP.SmartSchedue.Api.Contracts/Services/Integration/Output/CancelIntegrationRecordResult.cs +++ b/NPP.SmartSchedue.Api.Contracts/Services/Integration/Output/CancelIntegrationRecordResult.cs @@ -64,7 +64,7 @@ public class CancelIntegrationRecordResult /// /// 通知发送结果 /// - public List NotificationResults { get; set; } = new(); + public List NotificationResults { get; set; } = new(); /// /// 撤销统计信息 diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Integration/Output/ProcessTaskChangesResult.cs b/NPP.SmartSchedue.Api.Contracts/Services/Integration/Output/ProcessTaskChangesResult.cs index 8604500..d54074e 100644 --- a/NPP.SmartSchedue.Api.Contracts/Services/Integration/Output/ProcessTaskChangesResult.cs +++ b/NPP.SmartSchedue.Api.Contracts/Services/Integration/Output/ProcessTaskChangesResult.cs @@ -86,7 +86,7 @@ namespace NPP.SmartSchedue.Api.Contracts.Services.Integration.Output /// /// 通知发送结果 /// - public List NotificationResults { get; set; } = new(); + public List NotificationResults { get; set; } = new(); /// /// 警告信息 diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Integration/Output/PublishIntegrationRecordResult.cs b/NPP.SmartSchedue.Api.Contracts/Services/Integration/Output/PublishIntegrationRecordResult.cs index 452ced9..d8034b5 100644 --- a/NPP.SmartSchedue.Api.Contracts/Services/Integration/Output/PublishIntegrationRecordResult.cs +++ b/NPP.SmartSchedue.Api.Contracts/Services/Integration/Output/PublishIntegrationRecordResult.cs @@ -67,7 +67,7 @@ public class PublishIntegrationRecordResult /// 通知发送结果 /// 记录发布后通知的发送情况 /// - public List NotificationResults { get; set; } = new(); + public List NotificationResults { get; set; } = new(); /// /// 发布统计信息 @@ -272,9 +272,9 @@ public class PublishValidationResult } /// -/// 通知发送结果 +/// 整合通知发送结果 /// -public class NotificationSendResult +public class IntegrationNotificationResult { /// /// 通知类型 diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Integration/Output/SmartReallocationResult.cs b/NPP.SmartSchedue.Api.Contracts/Services/Integration/Output/SmartReallocationResult.cs index 49a2163..b090429 100644 --- a/NPP.SmartSchedue.Api.Contracts/Services/Integration/Output/SmartReallocationResult.cs +++ b/NPP.SmartSchedue.Api.Contracts/Services/Integration/Output/SmartReallocationResult.cs @@ -87,7 +87,7 @@ namespace NPP.SmartSchedue.Api.Contracts.Services.Integration.Output /// /// 通知发送结果 /// - public List NotificationResults { get; set; } = new(); + public List NotificationResults { get; set; } = new(); /// /// 验证结果 diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Integration/Output/VersionManagementResults.cs b/NPP.SmartSchedue.Api.Contracts/Services/Integration/Output/VersionManagementResults.cs index 86f3897..8b4b80b 100644 --- a/NPP.SmartSchedue.Api.Contracts/Services/Integration/Output/VersionManagementResults.cs +++ b/NPP.SmartSchedue.Api.Contracts/Services/Integration/Output/VersionManagementResults.cs @@ -336,7 +336,7 @@ namespace NPP.SmartSchedue.Api.Contracts.Services.Integration.Output /// /// 通知发送结果 /// - public List NotificationResults { get; set; } = new(); + public List NotificationResults { get; set; } = new(); /// /// 警告信息 diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Integration/Output/WorkOrderOperationResult.cs b/NPP.SmartSchedue.Api.Contracts/Services/Integration/Output/WorkOrderOperationResult.cs new file mode 100644 index 0000000..e043022 --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Integration/Output/WorkOrderOperationResult.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; + +namespace NPP.SmartSchedue.Api.Contracts.Services.Integration.Output +{ + /// + /// 操作结果输出 + /// + public class WorkOrderOperationResult + { + /// + /// 操作是否成功 + /// + public bool IsSuccess { get; set; } + + /// + /// 操作消息 + /// + public string Message { get; set; } = string.Empty; + + /// + /// 影响的整合记录ID + /// + public long? AffectedIntegrationRecordId { get; set; } + + /// + /// 操作详情数据 + /// + public object Data { get; set; } + + /// + /// 错误详情(操作失败时) + /// + public List ErrorDetails { get; set; } = new List(); + } +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Notification/IEmailNotificationService.cs b/NPP.SmartSchedue.Api.Contracts/Services/Notification/IEmailNotificationService.cs index 8593c8d..95baa89 100644 --- a/NPP.SmartSchedue.Api.Contracts/Services/Notification/IEmailNotificationService.cs +++ b/NPP.SmartSchedue.Api.Contracts/Services/Notification/IEmailNotificationService.cs @@ -1,6 +1,6 @@ using System.Threading.Tasks; -using System.Collections.Generic; -using NPP.SmartSchedue.Api.Contracts.Core.Enums; +using NPP.SmartSchedue.Api.Contracts.Services.Notification.Input; +using NPP.SmartSchedue.Api.Contracts.Services.Notification.Output; namespace NPP.SmartSchedue.Api.Contracts.Services.Notification; @@ -10,132 +10,33 @@ namespace NPP.SmartSchedue.Api.Contracts.Services.Notification; /// public interface IEmailNotificationService { - #region 单个邮件发送 - /// - /// 发送邮件通知 + /// 发送单个邮件 /// - /// 接收人邮箱 - /// 邮件主题 - /// 邮件内容 - /// 是否HTML格式 - /// - Task SendEmailAsync(string recipientEmail, string subject, string content, bool isHtml = true); + Task SendEmailAsync(SendEmailInput input); /// - /// 发送邮件通知(带附件) + /// 批量发送邮件 /// - /// 接收人邮箱 - /// 邮件主题 - /// 邮件内容 - /// 附件文件路径列表 - /// 是否HTML格式 - /// - Task SendEmailWithAttachmentsAsync( - string recipientEmail, - string subject, - string content, - List attachments, - bool isHtml = true); - - #endregion - - #region 批量邮件发送 - - /// - /// 批量发送邮件通知 - /// - /// 接收人邮箱列表 - /// 邮件主题 - /// 邮件内容 - /// 是否HTML格式 - /// 发送结果,Key为邮箱地址,Value为是否发送成功 - Task> BatchSendEmailAsync( - List recipients, - string subject, - string content, - bool isHtml = true); + Task BatchSendEmailAsync(BatchSendEmailInput input); /// - /// 个性化批量发送邮件通知 - /// 每个收件人可以有不同的邮件内容 + /// 个性化批量发送邮件 /// - /// 邮件项列表 - /// 发送结果,Key为邮箱地址,Value为是否发送成功 - Task> BatchSendPersonalizedEmailAsync(List emailItems); + Task BatchSendPersonalizedEmailAsync(BatchSendPersonalizedEmailInput input); - #endregion - - #region 邮件模板 - /// /// 使用模板发送邮件 /// - /// 接收人邮箱 - /// 邮件主题模板 - /// 邮件内容模板 - /// 模板变量 - /// 是否HTML格式 - /// - Task SendEmailByTemplateAsync( - string recipientEmail, - string subjectTemplate, - string contentTemplate, - Dictionary variables, - bool isHtml = true); + Task SendEmailByTemplateAsync(SendEmailByTemplateInput input); - #endregion - - #region 邮件发送状态检查 - /// /// 验证邮箱地址格式 /// - /// 邮箱地址 - /// - bool IsValidEmail(string email); + Task IsValidEmailAsync(string email); /// /// 检查邮件服务器连接状态 /// - /// - Task CheckEmailServerConnectionAsync(); - - #endregion -} - -/// -/// 邮件项 -/// -public class EmailItem -{ - /// - /// 接收人邮箱 - /// - public string RecipientEmail { get; set; } = ""; - - /// - /// 邮件主题 - /// - public string Subject { get; set; } = ""; - - /// - /// 邮件内容 - /// - public string Content { get; set; } = ""; - - /// - /// 是否HTML格式 - /// - public bool IsHtml { get; set; } = true; - - /// - /// 附件文件路径列表 - /// - public List Attachments { get; set; } = new List(); - - /// - /// 个性化变量 - /// - public Dictionary Variables { get; set; } = new Dictionary(); + Task CheckEmailServerStatusAsync(); } \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Notification/INotificationService.cs b/NPP.SmartSchedue.Api.Contracts/Services/Notification/INotificationService.cs index 20c3395..d29c618 100644 --- a/NPP.SmartSchedue.Api.Contracts/Services/Notification/INotificationService.cs +++ b/NPP.SmartSchedue.Api.Contracts/Services/Notification/INotificationService.cs @@ -29,7 +29,15 @@ public interface INotificationService /// /// /// - Task> GetNotificationSettingPageAsync(PageInput input); + Task> GetNotificationSettingPageAsync(PageInput input); + + /// + /// 获取通知设置列表(可选过滤,含缓存) + /// + /// 是否启用(可选) + /// 人员组ID(可选) + /// + Task> GetNotificationSettingListAsync(bool? isEnabled = null, long? personnelGroupId = null); /// /// 创建通知设置 @@ -76,7 +84,7 @@ public interface INotificationService /// /// /// - Task> GetPersonnelGroupPageAsync(PageInput input); + Task> GetPersonnelGroupPageAsync(PageInput input); /// /// 创建人员组 @@ -90,7 +98,7 @@ public interface INotificationService /// /// /// - Task UpdatePersonnelGroupAsync(PersonnelGroupCreateInput input); + Task UpdatePersonnelGroupAsync(PersonnelGroupUpdateInput input); /// /// 删除人员组 @@ -119,6 +127,13 @@ public interface INotificationService /// Task SendNotificationAsync(SendNotificationInput input); + /// + /// 发送群组通知(邮件发送一封给所有人,系统消息仍然单独发送) + /// + /// + /// + Task SendGroupNotificationAsync(SendNotificationInput input); + /// /// 批量发送通知 /// @@ -146,15 +161,6 @@ public interface INotificationService #region 通知模板引擎(决策点7) - /// - /// 渲染通知模板 - /// 支持通知内容模板,可替换变量 - /// - /// 模板内容 - /// 变量字典 - /// - Task RenderTemplateAsync(string template, Dictionary variables); - /// /// 验证模板语法 /// @@ -178,7 +184,7 @@ public interface INotificationService /// /// /// - Task> GetNotificationHistoryPageAsync(PageInput input); + Task> GetNotificationHistoryPageAsync(PageInput input); /// /// 重试失败的通知 @@ -194,6 +200,20 @@ public interface INotificationService /// Task BatchRetryFailedNotificationsAsync(List notificationHistoryIds); + /// + /// 取消通知 + /// + /// 通知历史记录ID + /// + Task CancelNotificationAsync(long notificationHistoryId); + + /// + /// 批量取消通知 + /// + /// 通知历史记录ID列表 + /// 成功取消的数量 + Task BatchCancelNotificationsAsync(List notificationHistoryIds); + #endregion #region 定时任务管理(决策点6) diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Notification/INotificationTemplateService.cs b/NPP.SmartSchedue.Api.Contracts/Services/Notification/INotificationTemplateService.cs index 6dd17ca..82b397a 100644 --- a/NPP.SmartSchedue.Api.Contracts/Services/Notification/INotificationTemplateService.cs +++ b/NPP.SmartSchedue.Api.Contracts/Services/Notification/INotificationTemplateService.cs @@ -1,5 +1,6 @@ -using System.Collections.Generic; using System.Threading.Tasks; +using NPP.SmartSchedue.Api.Contracts.Services.Notification.Input; +using NPP.SmartSchedue.Api.Contracts.Services.Notification.Output; namespace NPP.SmartSchedue.Api.Contracts.Services.Notification; @@ -15,18 +16,9 @@ public interface INotificationTemplateService /// 渲染通知模板 /// 支持变量替换,如:{变量名} /// - /// 模板内容 - /// 变量字典 - /// 渲染后的内容 - Task RenderTemplateAsync(string template, Dictionary variables); - - /// - /// 同步渲染通知模板 - /// - /// 模板内容 - /// 变量字典 - /// 渲染后的内容 - string RenderTemplate(string template, Dictionary variables); + /// 渲染模板输入参数 + /// 渲染结果 + Task RenderTemplateAsync(RenderTemplateInput input); #endregion @@ -35,16 +27,9 @@ public interface INotificationTemplateService /// /// 验证模板语法是否正确 /// - /// 模板内容 + /// 验证模板输入参数 /// 验证结果 - Task ValidateTemplateAsync(string template); - - /// - /// 同步验证模板语法 - /// - /// 模板内容 - /// 验证结果 - TemplateValidationResult ValidateTemplate(string template); + Task ValidateTemplateAsync(ValidateTemplateInput input); #endregion @@ -53,16 +38,9 @@ public interface INotificationTemplateService /// /// 提取模板中的变量列表 /// - /// 模板内容 - /// 变量名列表 - Task> ExtractVariablesAsync(string template); - - /// - /// 同步提取模板中的变量列表 - /// - /// 模板内容 - /// 变量名列表 - List ExtractVariables(string template); + /// 提取变量输入参数 + /// 变量列表 + Task ExtractVariablesAsync(ExtractVariablesInput input); #endregion @@ -71,20 +49,15 @@ public interface INotificationTemplateService /// /// 获取系统内置变量 /// - /// 内置变量字典 - Task> GetSystemVariablesAsync(); + /// 系统变量字典 + Task GetSystemVariablesAsync(); /// /// 获取业务相关变量 /// - /// 业务类型 - /// 业务ID - /// 业务数据 + /// 获取业务变量输入参数 /// 业务变量字典 - Task> GetBusinessVariablesAsync( - string businessType, - long? businessId = null, - string businessData = ""); + Task GetBusinessVariablesAsync(GetBusinessVariablesInput input); #endregion @@ -93,148 +66,17 @@ public interface INotificationTemplateService /// /// 获取预定义模板列表 /// - /// 模板分类 + /// 获取模板列表输入参数 /// 预定义模板列表 - Task> GetPredefinedTemplatesAsync(string category = ""); + Task GetPredefinedTemplatesAsync(GetPredefinedTemplatesInput input); /// /// 获取指定预定义模板 /// - /// 模板ID + /// 获取指定模板输入参数 /// 预定义模板 - Task GetPredefinedTemplateAsync(string templateId); + Task GetPredefinedTemplateAsync(GetPredefinedTemplateInput input); #endregion } -/// -/// 模板验证结果 -/// -public class TemplateValidationResult -{ - /// - /// 是否验证通过 - /// - public bool IsValid { get; set; } = true; - - /// - /// 错误信息列表 - /// - public List Errors { get; set; } = new List(); - - /// - /// 警告信息列表 - /// - public List Warnings { get; set; } = new List(); - - /// - /// 发现的变量列表 - /// - public List Variables { get; set; } = new List(); - - /// - /// 添加错误信息 - /// - /// 错误信息 - public void AddError(string error) - { - IsValid = false; - Errors.Add(error); - } - - /// - /// 添加警告信息 - /// - /// 警告信息 - public void AddWarning(string warning) - { - Warnings.Add(warning); - } -} - -/// -/// 预定义模板 -/// -public class PredefinedTemplate -{ - /// - /// 模板ID - /// - public string TemplateId { get; set; } = ""; - - /// - /// 模板名称 - /// - public string TemplateName { get; set; } = ""; - - /// - /// 模板分类 - /// - public string Category { get; set; } = ""; - - /// - /// 模板描述 - /// - public string Description { get; set; } = ""; - - /// - /// 邮件主题模板 - /// - public string EmailSubjectTemplate { get; set; } = ""; - - /// - /// 邮件内容模板 - /// - public string EmailContentTemplate { get; set; } = ""; - - /// - /// 系统消息标题模板 - /// - public string SystemMessageTitleTemplate { get; set; } = ""; - - /// - /// 系统消息内容模板 - /// - public string SystemMessageContentTemplate { get; set; } = ""; - - /// - /// 支持的变量列表 - /// - public List SupportedVariables { get; set; } = new List(); -} - -/// -/// 模板变量定义 -/// -public class TemplateVariable -{ - /// - /// 变量名 - /// - public string Name { get; set; } = ""; - - /// - /// 变量描述 - /// - public string Description { get; set; } = ""; - - /// - /// 变量类型 - /// - public string Type { get; set; } = "string"; - - /// - /// 是否必需 - /// - public bool IsRequired { get; set; } = false; - - /// - /// 默认值 - /// - public string DefaultValue { get; set; } = ""; - - /// - /// 示例值 - /// - public string ExampleValue { get; set; } = ""; -} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Notification/ISystemMessageService.cs b/NPP.SmartSchedue.Api.Contracts/Services/Notification/ISystemMessageService.cs index d4488a0..b4f304c 100644 --- a/NPP.SmartSchedue.Api.Contracts/Services/Notification/ISystemMessageService.cs +++ b/NPP.SmartSchedue.Api.Contracts/Services/Notification/ISystemMessageService.cs @@ -1,6 +1,7 @@ using System.Threading.Tasks; using System.Collections.Generic; -using NPP.SmartSchedue.Api.Contracts.Core.Enums; +using NPP.SmartSchedue.Api.Contracts.Services.Notification.Input; +using NPP.SmartSchedue.Api.Contracts.Services.Notification.Output; namespace NPP.SmartSchedue.Api.Contracts.Services.Notification; @@ -15,40 +16,16 @@ public interface ISystemMessageService /// /// 发送系统消息 /// - /// 接收人员ID - /// 消息标题 - /// 消息内容 - /// 消息类型(通知、警告、错误等) - /// 业务类型 - /// 业务ID - /// - Task SendSystemMessageAsync( - long recipientPersonnelId, - string title, - string content, - SystemMessageTypeEnum messageType = SystemMessageTypeEnum.Info, - string businessType = "", - long? businessId = null); + /// 发送系统消息输入模型 + /// 发送结果 + Task SendSystemMessageAsync(SendSystemMessageInput input); /// /// 发送带操作按钮的系统消息 /// - /// 接收人员ID - /// 消息标题 - /// 消息内容 - /// 操作按钮列表 - /// 消息类型 - /// 业务类型 - /// 业务ID - /// - Task SendSystemMessageWithActionsAsync( - long recipientPersonnelId, - string title, - string content, - List actions, - SystemMessageTypeEnum messageType = SystemMessageTypeEnum.Info, - string businessType = "", - long? businessId = null); + /// 发送带操作按钮的系统消息输入模型 + /// 发送结果 + Task SendSystemMessageWithActionsAsync(SendSystemMessageWithActionsInput input); #endregion @@ -57,28 +34,17 @@ public interface ISystemMessageService /// /// 批量发送系统消息 /// - /// 接收人员ID列表 - /// 消息标题 - /// 消息内容 - /// 消息类型 - /// 业务类型 - /// 业务ID - /// 发送结果,Key为人员ID,Value为是否发送成功 - Task> BatchSendSystemMessageAsync( - List recipientPersonnelIds, - string title, - string content, - SystemMessageTypeEnum messageType = SystemMessageTypeEnum.Info, - string businessType = "", - long? businessId = null); + /// 批量发送系统消息输入模型 + /// 批量发送结果 + Task BatchSendSystemMessageAsync(BatchSendSystemMessageInput input); /// /// 个性化批量发送系统消息 /// 每个收件人可以有不同的消息内容 /// /// 消息项列表 - /// 发送结果,Key为人员ID,Value为是否发送成功 - Task> BatchSendPersonalizedSystemMessageAsync(List messageItems); + /// 批量发送结果 + Task BatchSendPersonalizedSystemMessageAsync(List messageItems); #endregion @@ -87,22 +53,9 @@ public interface ISystemMessageService /// /// 使用模板发送系统消息 /// - /// 接收人员ID - /// 消息标题模板 - /// 消息内容模板 - /// 模板变量 - /// 消息类型 - /// 业务类型 - /// 业务ID - /// - Task SendSystemMessageByTemplateAsync( - long recipientPersonnelId, - string titleTemplate, - string contentTemplate, - Dictionary variables, - SystemMessageTypeEnum messageType = SystemMessageTypeEnum.Info, - string businessType = "", - long? businessId = null); + /// 使用模板发送系统消息输入模型 + /// 发送结果 + Task SendSystemMessageByTemplateAsync(SendSystemMessageByTemplateInput input); #endregion @@ -113,139 +66,31 @@ public interface ISystemMessageService /// /// 消息ID /// 接收人员ID - /// - Task MarkMessageAsReadAsync(long messageId, long recipientPersonnelId); + /// 操作结果 + Task MarkMessageAsReadAsync(long messageId, long recipientPersonnelId); /// /// 批量标记消息为已读 /// /// 消息ID列表 /// 接收人员ID - /// - Task BatchMarkMessagesAsReadAsync(List messageIds, long recipientPersonnelId); + /// 操作结果 + Task BatchMarkMessagesAsReadAsync(List messageIds, long recipientPersonnelId); /// /// 删除消息 /// /// 消息ID /// 接收人员ID - /// - Task DeleteMessageAsync(long messageId, long recipientPersonnelId); + /// 操作结果 + Task DeleteMessageAsync(long messageId, long recipientPersonnelId); /// /// 获取用户未读消息数量 /// /// 接收人员ID - /// + /// 未读消息数量 Task GetUnreadMessageCountAsync(long recipientPersonnelId); #endregion -} - -/// -/// 系统消息类型枚举 -/// -public enum SystemMessageTypeEnum -{ - /// - /// 信息 - /// - Info = 1, - - /// - /// 成功 - /// - Success = 2, - - /// - /// 警告 - /// - Warning = 3, - - /// - /// 错误 - /// - Error = 4, - - /// - /// 紧急 - /// - Urgent = 5 -} - -/// -/// 系统消息项 -/// -public class SystemMessageItem -{ - /// - /// 接收人员ID - /// - public long RecipientPersonnelId { get; set; } - - /// - /// 消息标题 - /// - public string Title { get; set; } = ""; - - /// - /// 消息内容 - /// - public string Content { get; set; } = ""; - - /// - /// 消息类型 - /// - public SystemMessageTypeEnum MessageType { get; set; } = SystemMessageTypeEnum.Info; - - /// - /// 业务类型 - /// - public string BusinessType { get; set; } = ""; - - /// - /// 业务ID - /// - public long? BusinessId { get; set; } - - /// - /// 操作按钮列表 - /// - public List Actions { get; set; } = new List(); - - /// - /// 个性化变量 - /// - public Dictionary Variables { get; set; } = new Dictionary(); -} - -/// -/// 系统消息操作按钮 -/// -public class SystemMessageAction -{ - /// - /// 操作ID - /// - public string ActionId { get; set; } = ""; - - /// - /// 操作名称 - /// - public string ActionName { get; set; } = ""; - - /// - /// 操作URL - /// - public string ActionUrl { get; set; } = ""; - - /// - /// 操作类型(按钮、链接等) - /// - public string ActionType { get; set; } = "button"; - - /// - /// 是否主要操作 - /// - public bool IsPrimary { get; set; } = false; } \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/BatchSendEmailInput.cs b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/BatchSendEmailInput.cs new file mode 100644 index 0000000..5812510 --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/BatchSendEmailInput.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Input; + +/// +/// 批量发送邮件输入 +/// +public class BatchSendEmailInput +{ + /// + /// 接收人邮箱列表 + /// + [Required(ErrorMessage = "接收人邮箱列表不能为空")] + [MinLength(1, ErrorMessage = "至少需要一个接收人")] + public List Recipients { get; set; } = new List(); + + /// + /// 邮件主题 + /// + [Required(ErrorMessage = "邮件主题不能为空")] + [MaxLength(500, ErrorMessage = "邮件主题长度不能超过500个字符")] + public string Subject { get; set; } = ""; + + /// + /// 邮件内容 + /// + [Required(ErrorMessage = "邮件内容不能为空")] + public string Content { get; set; } = ""; + + /// + /// 是否HTML格式 + /// + public bool IsHtml { get; set; } = true; +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/BatchSendPersonalizedEmailInput.cs b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/BatchSendPersonalizedEmailInput.cs new file mode 100644 index 0000000..fcfd6d1 --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/BatchSendPersonalizedEmailInput.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Input; + +/// +/// 个性化批量发送邮件输入 +/// +public class BatchSendPersonalizedEmailInput +{ + /// + /// 邮件项列表 + /// + [Required(ErrorMessage = "邮件项列表不能为空")] + [MinLength(1, ErrorMessage = "至少需要一个邮件项")] + public List EmailItems { get; set; } = new List(); +} + +/// +/// 邮件项 +/// +public class EmailItem +{ + /// + /// 接收人邮箱 + /// + [Required(ErrorMessage = "接收人邮箱不能为空")] + [EmailAddress(ErrorMessage = "邮箱地址格式无效")] + public string RecipientEmail { get; set; } = ""; + + /// + /// 邮件主题 + /// + [Required(ErrorMessage = "邮件主题不能为空")] + [MaxLength(500, ErrorMessage = "邮件主题长度不能超过500个字符")] + public string Subject { get; set; } = ""; + + /// + /// 邮件内容 + /// + [Required(ErrorMessage = "邮件内容不能为空")] + public string Content { get; set; } = ""; + + /// + /// 是否HTML格式 + /// + public bool IsHtml { get; set; } = true; + + /// + /// 附件文件路径列表 + /// + public List? Attachments { get; set; } +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/BatchSendSystemMessageInput.cs b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/BatchSendSystemMessageInput.cs new file mode 100644 index 0000000..d899101 --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/BatchSendSystemMessageInput.cs @@ -0,0 +1,47 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using NPP.SmartSchedue.Api.Contracts.Core.Enums; + +namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Input; + +/// +/// 批量发送系统消息输入模型 +/// +public class BatchSendSystemMessageInput +{ + /// + /// 接收人员ID列表 + /// + [Required(ErrorMessage = "接收人员ID列表不能为空")] + public List RecipientPersonnelIds { get; set; } = new List(); + + /// + /// 消息标题 + /// + [Required(ErrorMessage = "消息标题不能为空")] + [StringLength(200, ErrorMessage = "消息标题长度不能超过200个字符")] + public string Title { get; set; } = ""; + + /// + /// 消息内容 + /// + [Required(ErrorMessage = "消息内容不能为空")] + [StringLength(4000, ErrorMessage = "消息内容长度不能超过4000个字符")] + public string Content { get; set; } = ""; + + /// + /// 消息类型 + /// + public SystemMessageTypeEnum MessageType { get; set; } = SystemMessageTypeEnum.Info; + + /// + /// 业务类型 + /// + [StringLength(100, ErrorMessage = "业务类型长度不能超过100个字符")] + public string BusinessType { get; set; } = ""; + + /// + /// 业务ID + /// + public long? BusinessId { get; set; } +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/ExtractVariablesInput.cs b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/ExtractVariablesInput.cs new file mode 100644 index 0000000..523dbee --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/ExtractVariablesInput.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Input; + +/// +/// 提取变量输入模型 +/// +public class ExtractVariablesInput +{ + /// + /// 模板内容 + /// + [Required(ErrorMessage = "模板内容不能为空")] + public string Template { get; set; } = ""; +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/GetBusinessVariablesInput.cs b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/GetBusinessVariablesInput.cs new file mode 100644 index 0000000..4c8aa7c --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/GetBusinessVariablesInput.cs @@ -0,0 +1,25 @@ +using System.ComponentModel.DataAnnotations; + +namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Input; + +/// +/// 获取业务变量输入模型 +/// +public class GetBusinessVariablesInput +{ + /// + /// 业务类型 + /// + [Required(ErrorMessage = "业务类型不能为空")] + public string BusinessType { get; set; } = ""; + + /// + /// 业务ID + /// + public long? BusinessId { get; set; } + + /// + /// 业务数据 + /// + public string BusinessData { get; set; } = ""; +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/GetPredefinedTemplateInput.cs b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/GetPredefinedTemplateInput.cs new file mode 100644 index 0000000..64bff02 --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/GetPredefinedTemplateInput.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Input; + +/// +/// 获取指定预定义模板输入模型 +/// +public class GetPredefinedTemplateInput +{ + /// + /// 模板ID + /// + [Required(ErrorMessage = "模板ID不能为空")] + public string TemplateId { get; set; } = ""; +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/GetPredefinedTemplatesInput.cs b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/GetPredefinedTemplatesInput.cs new file mode 100644 index 0000000..8d810f9 --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/GetPredefinedTemplatesInput.cs @@ -0,0 +1,12 @@ +namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Input; + +/// +/// 获取预定义模板列表输入模型 +/// +public class GetPredefinedTemplatesInput +{ + /// + /// 模板分类 + /// + public string Category { get; set; } = ""; +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/NotificationHistoryPageInput.cs b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/NotificationHistoryPageInput.cs new file mode 100644 index 0000000..0b7ecfb --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/NotificationHistoryPageInput.cs @@ -0,0 +1,47 @@ +using System; +using NPP.SmartSchedue.Api.Contracts.Core.Enums; + +namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Input; + +public class NotificationHistoryPageInput +{ + /// + /// 通知设置ID + /// + public long? NotificationSettingId { get; set; } + + /// + /// 接收人员ID + /// + public long? RecipientPersonnelId { get; set; } + + /// + /// 接收人员姓名 + /// + public string RecipientPersonnelName { get; set; } = ""; + + /// + /// 通知方式 + /// + public NotificationTypeEnum? NotificationType { get; set; } + + /// + /// 发送状态 + /// + public NotificationStatusEnum? SendStatus { get; set; } + + /// + /// 业务类型 + /// + public string BusinessType { get; set; } = ""; + + /// + /// 开始时间 + /// + public DateTime? StartTime { get; set; } + + /// + /// 结束时间 + /// + public DateTime? EndTime { get; set; } +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/NotificationSettingPageInput.cs b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/NotificationSettingPageInput.cs new file mode 100644 index 0000000..62e42cc --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/NotificationSettingPageInput.cs @@ -0,0 +1,10 @@ +namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Input; + +public class NotificationSettingPageInput +{ + public string? NotificationName { get; set; } + + public bool? IsEnabled { get; set; } + + public long? PersonnelGroupId { get; set; } +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/PersonnelGroupGetPageInput.cs b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/PersonnelGroupGetPageInput.cs new file mode 100644 index 0000000..a43262f --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/PersonnelGroupGetPageInput.cs @@ -0,0 +1,18 @@ +using NPP.SmartSchedue.Api.Contracts.Core.Enums; + +namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Input; + +public class PersonnelGroupGetPageInput +{ + public string GroupName { get; set; } = ""; + + /// + /// 人员组类型 + /// + public PersonnelGroupTypeEnum? GroupType { get; set; } + + /// + /// 是否启用 + /// + public bool? IsEnabled { get; set; } +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/PersonnelGroupUpdateInput.cs b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/PersonnelGroupUpdateInput.cs new file mode 100644 index 0000000..0c253bb --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/PersonnelGroupUpdateInput.cs @@ -0,0 +1,16 @@ +using System.ComponentModel.DataAnnotations; +using NPP.SmartSchedue.Api.Contracts.Core.Enums; + +namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Input; + +/// +/// 更新人员组输入 +/// +public class PersonnelGroupUpdateInput : PersonnelGroupCreateInput +{ + /// + /// 人员组ID + /// + [Required(ErrorMessage = "人员组ID不能为空")] + public long Id { get; set; } +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/RenderTemplateInput.cs b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/RenderTemplateInput.cs new file mode 100644 index 0000000..3dafe5c --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/RenderTemplateInput.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Input; + +/// +/// 渲染模板输入模型 +/// +public class RenderTemplateInput +{ + /// + /// 模板内容 + /// + [Required(ErrorMessage = "模板内容不能为空")] + public string Template { get; set; } = ""; + + /// + /// 变量字典 + /// + public Dictionary Variables { get; set; } = new Dictionary(); +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/SendEmailByTemplateInput.cs b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/SendEmailByTemplateInput.cs new file mode 100644 index 0000000..e562d1e --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/SendEmailByTemplateInput.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Input; + +/// +/// 使用模板发送邮件输入 +/// +public class SendEmailByTemplateInput +{ + /// + /// 接收人邮箱 + /// + [Required(ErrorMessage = "接收人邮箱不能为空")] + [EmailAddress(ErrorMessage = "邮箱地址格式无效")] + public string RecipientEmail { get; set; } = ""; + + /// + /// 邮件主题模板 + /// + [Required(ErrorMessage = "邮件主题模板不能为空")] + public string SubjectTemplate { get; set; } = ""; + + /// + /// 邮件内容模板 + /// + [Required(ErrorMessage = "邮件内容模板不能为空")] + public string ContentTemplate { get; set; } = ""; + + /// + /// 模板变量 + /// + public Dictionary Variables { get; set; } = new Dictionary(); + + /// + /// 是否HTML格式 + /// + public bool IsHtml { get; set; } = true; +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/SendEmailInput.cs b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/SendEmailInput.cs new file mode 100644 index 0000000..838349d --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/SendEmailInput.cs @@ -0,0 +1,40 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; + +namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Input; + +/// +/// 发送邮件输入 +/// +public class SendEmailInput +{ + /// + /// 接收人邮箱 + /// + [Required(ErrorMessage = "接收人邮箱不能为空")] + [EmailAddress(ErrorMessage = "邮箱地址格式无效")] + public string RecipientEmail { get; set; } = ""; + + /// + /// 邮件主题 + /// + [Required(ErrorMessage = "邮件主题不能为空")] + [MaxLength(500, ErrorMessage = "邮件主题长度不能超过500个字符")] + public string Subject { get; set; } = ""; + + /// + /// 邮件内容 + /// + [Required(ErrorMessage = "邮件内容不能为空")] + public string Content { get; set; } = ""; + + /// + /// 是否HTML格式 + /// + public bool IsHtml { get; set; } = true; + + /// + /// 附件文件路径列表 + /// + public List? Attachments { get; set; } +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/SendNotificationInput.cs b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/SendNotificationInput.cs index c0a3fc2..18f0b71 100644 --- a/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/SendNotificationInput.cs +++ b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/SendNotificationInput.cs @@ -1,3 +1,4 @@ +using System; using System.Collections.Generic; using System.ComponentModel.DataAnnotations; using NPP.SmartSchedue.Api.Contracts.Core.Enums; @@ -16,7 +17,9 @@ public class SendNotificationInput /// [Required(ErrorMessage = "通知方式不能为空")] public NotificationTypeEnum NotificationType { get; set; } - + + public long SettingId { get; set; } + /// /// 通知标题 /// diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/SendSystemMessageByTemplateInput.cs b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/SendSystemMessageByTemplateInput.cs new file mode 100644 index 0000000..98ab527 --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/SendSystemMessageByTemplateInput.cs @@ -0,0 +1,53 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using NPP.SmartSchedue.Api.Contracts.Core.Enums; + +namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Input; + +/// +/// 使用模板发送系统消息输入模型 +/// +public class SendSystemMessageByTemplateInput +{ + /// + /// 接收人员ID + /// + [Required(ErrorMessage = "接收人员ID不能为空")] + [Range(1, long.MaxValue, ErrorMessage = "接收人员ID必须大于0")] + public long RecipientPersonnelId { get; set; } + + /// + /// 消息标题模板 + /// + [Required(ErrorMessage = "消息标题模板不能为空")] + [StringLength(200, ErrorMessage = "消息标题模板长度不能超过200个字符")] + public string TitleTemplate { get; set; } = ""; + + /// + /// 消息内容模板 + /// + [Required(ErrorMessage = "消息内容模板不能为空")] + [StringLength(4000, ErrorMessage = "消息内容模板长度不能超过4000个字符")] + public string ContentTemplate { get; set; } = ""; + + /// + /// 模板变量 + /// + public Dictionary Variables { get; set; } = new Dictionary(); + + /// + /// 消息类型 + /// + public SystemMessageTypeEnum MessageType { get; set; } = SystemMessageTypeEnum.Info; + + /// + /// 业务类型 + /// + [StringLength(100, ErrorMessage = "业务类型长度不能超过100个字符")] + public string BusinessType { get; set; } = ""; + + /// + /// 业务ID + /// + public long? BusinessId { get; set; } +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/SendSystemMessageInput.cs b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/SendSystemMessageInput.cs new file mode 100644 index 0000000..87ebf57 --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/SendSystemMessageInput.cs @@ -0,0 +1,49 @@ +using System.ComponentModel.DataAnnotations; +using NPP.SmartSchedue.Api.Contracts.Core.Enums; + +namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Input; + +/// +/// 发送系统消息输入模型 +/// +public class SendSystemMessageInput +{ + /// + /// 接收人员ID + /// + [Required(ErrorMessage = "接收人员ID不能为空")] + [Range(1, long.MaxValue, ErrorMessage = "接收人员ID必须大于0")] + public long RecipientPersonnelId { get; set; } + + public long NotificationSettingId { get; set; } + + /// + /// 消息标题 + /// + [Required(ErrorMessage = "消息标题不能为空")] + [StringLength(200, ErrorMessage = "消息标题长度不能超过200个字符")] + public string Title { get; set; } = ""; + + /// + /// 消息内容 + /// + [Required(ErrorMessage = "消息内容不能为空")] + [StringLength(4000, ErrorMessage = "消息内容长度不能超过4000个字符")] + public string Content { get; set; } = ""; + + /// + /// 消息类型 + /// + public SystemMessageTypeEnum MessageType { get; set; } = SystemMessageTypeEnum.Info; + + /// + /// 业务类型 + /// + [StringLength(100, ErrorMessage = "业务类型长度不能超过100个字符")] + public string BusinessType { get; set; } = ""; + + /// + /// 业务ID + /// + public long? BusinessId { get; set; } +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/SendSystemMessageWithActionsInput.cs b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/SendSystemMessageWithActionsInput.cs new file mode 100644 index 0000000..264255c --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/SendSystemMessageWithActionsInput.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; +using System.ComponentModel.DataAnnotations; +using NPP.SmartSchedue.Api.Contracts.Core.Enums; +using NPP.SmartSchedue.Api.Contracts.Services.Notification.Output; + +namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Input; + +/// +/// 发送带操作按钮的系统消息输入模型 +/// +public class SendSystemMessageWithActionsInput +{ + /// + /// 接收人员ID + /// + [Required(ErrorMessage = "接收人员ID不能为空")] + [Range(1, long.MaxValue, ErrorMessage = "接收人员ID必须大于0")] + public long RecipientPersonnelId { get; set; } + + /// + /// 消息标题 + /// + [Required(ErrorMessage = "消息标题不能为空")] + [StringLength(200, ErrorMessage = "消息标题长度不能超过200个字符")] + public string Title { get; set; } = ""; + + /// + /// 消息内容 + /// + [Required(ErrorMessage = "消息内容不能为空")] + [StringLength(4000, ErrorMessage = "消息内容长度不能超过4000个字符")] + public string Content { get; set; } = ""; + + /// + /// 操作按钮列表 + /// + public List Actions { get; set; } = new List(); + + /// + /// 消息类型 + /// + public SystemMessageTypeEnum MessageType { get; set; } = SystemMessageTypeEnum.Info; + + /// + /// 业务类型 + /// + [StringLength(100, ErrorMessage = "业务类型长度不能超过100个字符")] + public string BusinessType { get; set; } = ""; + + /// + /// 业务ID + /// + public long? BusinessId { get; set; } +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/ValidateTemplateInput.cs b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/ValidateTemplateInput.cs new file mode 100644 index 0000000..246998d --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Input/ValidateTemplateInput.cs @@ -0,0 +1,15 @@ +using System.ComponentModel.DataAnnotations; + +namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Input; + +/// +/// 验证模板输入模型 +/// +public class ValidateTemplateInput +{ + /// + /// 模板内容 + /// + [Required(ErrorMessage = "模板内容不能为空")] + public string Template { get; set; } = ""; +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/BatchSendEmailOutput.cs b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/BatchSendEmailOutput.cs new file mode 100644 index 0000000..2522f29 --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/BatchSendEmailOutput.cs @@ -0,0 +1,45 @@ +using System; +using System.Collections.Generic; + +namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Output; + +/// +/// 批量发送邮件输出 +/// +public class BatchSendEmailOutput +{ + /// + /// 总发送数量 + /// + public int TotalCount { get; set; } + + /// + /// 成功发送数量 + /// + public int SuccessCount { get; set; } + + /// + /// 失败发送数量 + /// + public int FailedCount { get; set; } + + /// + /// 发送时间 + /// + public DateTime SendTime { get; set; } = DateTime.Now; + + /// + /// 详细发送结果 + /// + public List Results { get; set; } = new List(); + + /// + /// 整体是否成功(所有邮件都发送成功) + /// + public bool IsAllSuccess => FailedCount == 0; + + /// + /// 成功率 + /// + public decimal SuccessRate => TotalCount > 0 ? (decimal)SuccessCount / TotalCount : 0; +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/BatchSendSystemMessageOutput.cs b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/BatchSendSystemMessageOutput.cs new file mode 100644 index 0000000..fee4b19 --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/BatchSendSystemMessageOutput.cs @@ -0,0 +1,64 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Output; + +/// +/// 批量发送系统消息输出模型 +/// +public class BatchSendSystemMessageOutput +{ + /// + /// 总发送数量 + /// + public int TotalCount { get; set; } + + /// + /// 成功发送数量 + /// + public int SuccessCount { get; set; } + + /// + /// 失败发送数量 + /// + public int FailureCount { get; set; } + + /// + /// 详细发送结果 + /// Key: 人员ID, Value: 发送结果 + /// + public Dictionary Results { get; set; } = new Dictionary(); + + /// + /// 批量发送时间 + /// + public DateTime SentTime { get; set; } + + /// + /// 是否全部发送成功 + /// + public bool AllSuccess => FailureCount == 0; + + /// + /// 成功率(百分比) + /// + public double SuccessRate => TotalCount > 0 ? (double)SuccessCount / TotalCount * 100 : 0; + + /// + /// 创建批量发送结果 + /// + public static BatchSendSystemMessageOutput Create(Dictionary results) + { + var output = new BatchSendSystemMessageOutput + { + Results = results, + TotalCount = results.Count, + SuccessCount = results.Values.Count(r => r.Success), + SentTime = DateTime.Now + }; + + output.FailureCount = output.TotalCount - output.SuccessCount; + return output; + } +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/EmailItem.cs b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/EmailItem.cs new file mode 100644 index 0000000..903b7aa --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/EmailItem.cs @@ -0,0 +1,39 @@ +using System.Collections.Generic; + +namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Output; + +/// +/// 邮件项 +/// +public class EmailItem +{ + /// + /// 接收人邮箱 + /// + public string RecipientEmail { get; set; } = ""; + + /// + /// 邮件主题 + /// + public string Subject { get; set; } = ""; + + /// + /// 邮件内容 + /// + public string Content { get; set; } = ""; + + /// + /// 是否HTML格式 + /// + public bool IsHtml { get; set; } = true; + + /// + /// 附件文件路径列表 + /// + public List Attachments { get; set; } = new List(); + + /// + /// 个性化变量 + /// + public Dictionary Variables { get; set; } = new Dictionary(); +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/EmailServerStatusOutput.cs b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/EmailServerStatusOutput.cs new file mode 100644 index 0000000..ca9695e --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/EmailServerStatusOutput.cs @@ -0,0 +1,39 @@ +using System; + +namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Output; + +/// +/// 邮件服务器状态输出 +/// +public class EmailServerStatusOutput +{ + /// + /// 服务器是否可用 + /// + public bool IsAvailable { get; set; } + + /// + /// SMTP服务器地址 + /// + public string SmtpServer { get; set; } = ""; + + /// + /// SMTP端口 + /// + public int SmtpPort { get; set; } + + /// + /// 检查时间 + /// + public DateTime CheckTime { get; set; } = DateTime.Now; + + /// + /// 响应时间(毫秒) + /// + public int ResponseTimeMs { get; set; } + + /// + /// 错误消息 + /// + public string? ErrorMessage { get; set; } +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/ExtractVariablesOutput.cs b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/ExtractVariablesOutput.cs new file mode 100644 index 0000000..2c16eee --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/ExtractVariablesOutput.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; + +namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Output; + +/// +/// 提取变量输出模型 +/// +public class ExtractVariablesOutput +{ + /// + /// 提取到的变量列表 + /// + public List Variables { get; set; } = new List(); + + /// + /// 是否提取成功 + /// + public bool Success { get; set; } = true; + + /// + /// 错误信息 + /// + public string ErrorMessage { get; set; } = ""; +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/GetBusinessVariablesOutput.cs b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/GetBusinessVariablesOutput.cs new file mode 100644 index 0000000..561271c --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/GetBusinessVariablesOutput.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; + +namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Output; + +/// +/// 获取业务变量输出模型 +/// +public class GetBusinessVariablesOutput +{ + /// + /// 业务变量字典 + /// + public Dictionary Variables { get; set; } = new Dictionary(); + + /// + /// 是否获取成功 + /// + public bool Success { get; set; } = true; + + /// + /// 错误信息 + /// + public string ErrorMessage { get; set; } = ""; +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/GetPredefinedTemplateOutput.cs b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/GetPredefinedTemplateOutput.cs new file mode 100644 index 0000000..3acac2c --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/GetPredefinedTemplateOutput.cs @@ -0,0 +1,22 @@ +namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Output; + +/// +/// 获取指定预定义模板输出模型 +/// +public class GetPredefinedTemplateOutput +{ + /// + /// 预定义模板 + /// + public PredefinedTemplate? Template { get; set; } + + /// + /// 是否获取成功 + /// + public bool Success { get; set; } = true; + + /// + /// 错误信息 + /// + public string ErrorMessage { get; set; } = ""; +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/GetPredefinedTemplatesOutput.cs b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/GetPredefinedTemplatesOutput.cs new file mode 100644 index 0000000..34cc883 --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/GetPredefinedTemplatesOutput.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; + +namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Output; + +/// +/// 获取预定义模板列表输出模型 +/// +public class GetPredefinedTemplatesOutput +{ + /// + /// 预定义模板列表 + /// + public List Templates { get; set; } = new List(); + + /// + /// 模板总数 + /// + public int TotalCount { get; set; } + + /// + /// 是否获取成功 + /// + public bool Success { get; set; } = true; + + /// + /// 错误信息 + /// + public string ErrorMessage { get; set; } = ""; +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/GetSystemVariablesOutput.cs b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/GetSystemVariablesOutput.cs new file mode 100644 index 0000000..bd1bd31 --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/GetSystemVariablesOutput.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; + +namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Output; + +/// +/// 获取系统变量输出模型 +/// +public class GetSystemVariablesOutput +{ + /// + /// 系统变量字典 + /// + public Dictionary Variables { get; set; } = new Dictionary(); + + /// + /// 是否获取成功 + /// + public bool Success { get; set; } = true; + + /// + /// 错误信息 + /// + public string ErrorMessage { get; set; } = ""; +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/PredefinedTemplate.cs b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/PredefinedTemplate.cs new file mode 100644 index 0000000..37943f6 --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/PredefinedTemplate.cs @@ -0,0 +1,54 @@ +using System.Collections.Generic; + +namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Output; + +/// +/// 预定义模板 +/// +public class PredefinedTemplate +{ + /// + /// 模板ID + /// + public string TemplateId { get; set; } = ""; + + /// + /// 模板名称 + /// + public string TemplateName { get; set; } = ""; + + /// + /// 模板分类 + /// + public string Category { get; set; } = ""; + + /// + /// 模板描述 + /// + public string Description { get; set; } = ""; + + /// + /// 邮件主题模板 + /// + public string EmailSubjectTemplate { get; set; } = ""; + + /// + /// 邮件内容模板 + /// + public string EmailContentTemplate { get; set; } = ""; + + /// + /// 系统消息标题模板 + /// + public string SystemMessageTitleTemplate { get; set; } = ""; + + /// + /// 系统消息内容模板 + /// + public string SystemMessageContentTemplate { get; set; } = ""; + + /// + /// 支持的变量列表 + /// + public List SupportedVariables { get; set; } = new List(); +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/RenderTemplateOutput.cs b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/RenderTemplateOutput.cs new file mode 100644 index 0000000..e2b40ee --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/RenderTemplateOutput.cs @@ -0,0 +1,22 @@ +namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Output; + +/// +/// 渲染模板输出模型 +/// +public class RenderTemplateOutput +{ + /// + /// 渲染后的内容 + /// + public string RenderedContent { get; set; } = ""; + + /// + /// 是否渲染成功 + /// + public bool Success { get; set; } = true; + + /// + /// 错误信息 + /// + public string ErrorMessage { get; set; } = ""; +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/SendEmailOutput.cs b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/SendEmailOutput.cs new file mode 100644 index 0000000..da4b5d1 --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/SendEmailOutput.cs @@ -0,0 +1,34 @@ +using System; + +namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Output; + +/// +/// 发送邮件输出 +/// +public class SendEmailOutput +{ + /// + /// 是否发送成功 + /// + public bool IsSuccess { get; set; } + + /// + /// 错误消息 + /// + public string? ErrorMessage { get; set; } + + /// + /// 发送时间 + /// + public DateTime SendTime { get; set; } = DateTime.Now; + + /// + /// 接收人邮箱 + /// + public string RecipientEmail { get; set; } = ""; + + /// + /// 邮件主题 + /// + public string Subject { get; set; } = ""; +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/SendSystemMessageOutput.cs b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/SendSystemMessageOutput.cs new file mode 100644 index 0000000..f537ad9 --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/SendSystemMessageOutput.cs @@ -0,0 +1,61 @@ +using System; + +namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Output; + +/// +/// 发送系统消息输出模型 +/// +public class SendSystemMessageOutput +{ + /// + /// 是否发送成功 + /// + public bool Success { get; set; } + + /// + /// 消息ID(发送成功时返回) + /// + public long? MessageId { get; set; } + + /// + /// 错误消息(发送失败时返回) + /// + public string ErrorMessage { get; set; } = ""; + + /// + /// 发送时间 + /// + public DateTime? SentTime { get; set; } + + /// + /// 接收人员ID + /// + public long RecipientPersonnelId { get; set; } + + /// + /// 创建成功结果 + /// + public static SendSystemMessageOutput CreateSuccess(long messageId, long recipientPersonnelId, DateTime sentTime) + { + return new SendSystemMessageOutput + { + Success = true, + MessageId = messageId, + RecipientPersonnelId = recipientPersonnelId, + SentTime = sentTime + }; + } + + /// + /// 创建失败结果 + /// + public static SendSystemMessageOutput CreateFailure(long recipientPersonnelId, string errorMessage) + { + return new SendSystemMessageOutput + { + Success = false, + RecipientPersonnelId = recipientPersonnelId, + ErrorMessage = errorMessage + }; + } +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/SystemMessageAction.cs b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/SystemMessageAction.cs new file mode 100644 index 0000000..fdc4e30 --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/SystemMessageAction.cs @@ -0,0 +1,32 @@ +namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Output; + +/// +/// 系统消息操作按钮 +/// +public class SystemMessageAction +{ + /// + /// 操作ID + /// + public string ActionId { get; set; } = ""; + + /// + /// 操作名称 + /// + public string ActionName { get; set; } = ""; + + /// + /// 操作URL + /// + public string ActionUrl { get; set; } = ""; + + /// + /// 操作类型(按钮、链接等) + /// + public string ActionType { get; set; } = "button"; + + /// + /// 是否主要操作 + /// + public bool IsPrimary { get; set; } = false; +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/SystemMessageItem.cs b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/SystemMessageItem.cs new file mode 100644 index 0000000..5f41b89 --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/SystemMessageItem.cs @@ -0,0 +1,50 @@ +using System.Collections.Generic; +using NPP.SmartSchedue.Api.Contracts.Core.Enums; + +namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Output; + +/// +/// 系统消息项 +/// +public class SystemMessageItem +{ + /// + /// 接收人员ID + /// + public long RecipientPersonnelId { get; set; } + + /// + /// 消息标题 + /// + public string Title { get; set; } = ""; + + /// + /// 消息内容 + /// + public string Content { get; set; } = ""; + + /// + /// 消息类型 + /// + public SystemMessageTypeEnum MessageType { get; set; } = SystemMessageTypeEnum.Info; + + /// + /// 业务类型 + /// + public string BusinessType { get; set; } = ""; + + /// + /// 业务ID + /// + public long? BusinessId { get; set; } + + /// + /// 操作按钮列表 + /// + public List Actions { get; set; } = new List(); + + /// + /// 个性化变量 + /// + public Dictionary Variables { get; set; } = new Dictionary(); +} diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/SystemMessageStatusOutput.cs b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/SystemMessageStatusOutput.cs new file mode 100644 index 0000000..6ed28ee --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/SystemMessageStatusOutput.cs @@ -0,0 +1,64 @@ +using System; + +namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Output; + +/// +/// 系统消息状态输出模型 +/// +public class SystemMessageStatusOutput +{ + /// + /// 操作是否成功 + /// + public bool Success { get; set; } + + /// + /// 操作结果消息 + /// + public string Message { get; set; } = ""; + + /// + /// 影响的消息数量 + /// + public int AffectedCount { get; set; } + + /// + /// 操作时间 + /// + public DateTime OperationTime { get; set; } + + /// + /// 人员ID + /// + public long PersonnelId { get; set; } + + /// + /// 创建成功结果 + /// + public static SystemMessageStatusOutput CreateSuccess(long personnelId, int affectedCount, string message = "操作成功") + { + return new SystemMessageStatusOutput + { + Success = true, + Message = message, + AffectedCount = affectedCount, + PersonnelId = personnelId, + OperationTime = DateTime.Now + }; + } + + /// + /// 创建失败结果 + /// + public static SystemMessageStatusOutput CreateFailure(long personnelId, string errorMessage) + { + return new SystemMessageStatusOutput + { + Success = false, + Message = errorMessage, + AffectedCount = 0, + PersonnelId = personnelId, + OperationTime = DateTime.Now + }; + } +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/TemplateValidationResult.cs b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/TemplateValidationResult.cs new file mode 100644 index 0000000..b6aa861 --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/TemplateValidationResult.cs @@ -0,0 +1,48 @@ +using System.Collections.Generic; + +namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Output; + +/// +/// 模板验证结果 +/// +public class TemplateValidationResult +{ + /// + /// 是否验证通过 + /// + public bool IsValid { get; set; } = true; + + /// + /// 错误信息列表 + /// + public List Errors { get; set; } = new List(); + + /// + /// 警告信息列表 + /// + public List Warnings { get; set; } = new List(); + + /// + /// 发现的变量列表 + /// + public List Variables { get; set; } = new List(); + + /// + /// 添加错误信息 + /// + /// 错误信息 + public void AddError(string error) + { + IsValid = false; + Errors.Add(error); + } + + /// + /// 添加警告信息 + /// + /// 警告信息 + public void AddWarning(string warning) + { + Warnings.Add(warning); + } +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/TemplateVariable.cs b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/TemplateVariable.cs new file mode 100644 index 0000000..84d409a --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/TemplateVariable.cs @@ -0,0 +1,37 @@ +namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Output; + +/// +/// 模板变量定义 +/// +public class TemplateVariable +{ + /// + /// 变量名 + /// + public string Name { get; set; } = ""; + + /// + /// 变量描述 + /// + public string Description { get; set; } = ""; + + /// + /// 变量类型 + /// + public string Type { get; set; } = "string"; + + /// + /// 是否必需 + /// + public bool IsRequired { get; set; } = false; + + /// + /// 默认值 + /// + public string DefaultValue { get; set; } = ""; + + /// + /// 示例值 + /// + public string ExampleValue { get; set; } = ""; +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/ValidateTemplateOutput.cs b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/ValidateTemplateOutput.cs new file mode 100644 index 0000000..0684e41 --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Notification/Output/ValidateTemplateOutput.cs @@ -0,0 +1,24 @@ +using System.Collections.Generic; + +namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Output; + +/// +/// 验证模板输出模型 +/// +public class ValidateTemplateOutput +{ + /// + /// 验证结果详情 + /// + public TemplateValidationResult ValidationResult { get; set; } = new TemplateValidationResult(); + + /// + /// 是否验证成功 + /// + public bool Success { get; set; } = true; + + /// + /// 错误信息 + /// + public string ErrorMessage { get; set; } = ""; +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Personnel/IPersonnelQualificationService.cs b/NPP.SmartSchedue.Api.Contracts/Services/Personnel/IPersonnelQualificationService.cs index 7a64bd8..dec646e 100644 --- a/NPP.SmartSchedue.Api.Contracts/Services/Personnel/IPersonnelQualificationService.cs +++ b/NPP.SmartSchedue.Api.Contracts/Services/Personnel/IPersonnelQualificationService.cs @@ -4,6 +4,7 @@ using NPP.SmartSchedue.Api.Contracts.Domain.Personnel; using ZhonTai.Admin.Core.Dto; using NPP.SmartSchedue.Api.Contracts.Services.Personnel.Input; using NPP.SmartSchedue.Api.Contracts.Services.Personnel.Output; +using System; namespace NPP.SmartSchedue.Api.Contracts.Services.Personnel; @@ -68,4 +69,20 @@ public interface IPersonnelQualificationService /// 人员ID /// 人员有效资质实体列表 Task> GetPersonnelQualificationsAsync(long personnelId); + + /// + /// 获取人员资质统计信息(横坐标为资质,纵坐标为人员数量) + /// + /// 人员资质统计结果 + Task GetPersonnelQualificationStatisticsAsync(); + + + /// + /// 获取即将过期的资质列表 + /// + /// + /// + /// + /// + Task> GetExpiringAsync(DateTime today, IList personnelIds = null, bool includeExpired = false); } \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Personnel/Output/ExpiringQualificationItemOutput.cs b/NPP.SmartSchedue.Api.Contracts/Services/Personnel/Output/ExpiringQualificationItemOutput.cs new file mode 100644 index 0000000..9186f4f --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Personnel/Output/ExpiringQualificationItemOutput.cs @@ -0,0 +1,20 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; + +namespace NPP.SmartSchedue.Api.Contracts.Services.Personnel.Output +{ + public class ExpiringQualificationItemOutput + { + public long PersonnelId { get; set; } + public string PersonnelName { get; set; } + public string PersonnelCode { get; set; } + public long QualificationId { get; set; } + public string QualificationLevel { get; set; } + public DateTime? ExpiryDate { get; set; } + public int DaysLeft { get; set; } + + public string QualificationName { get; set; } + } +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Work/IWorkOrderService.cs b/NPP.SmartSchedue.Api.Contracts/Services/Work/IWorkOrderService.cs index f9ed001..fe42377 100644 --- a/NPP.SmartSchedue.Api.Contracts/Services/Work/IWorkOrderService.cs +++ b/NPP.SmartSchedue.Api.Contracts/Services/Work/IWorkOrderService.cs @@ -5,6 +5,7 @@ using ZhonTai.Admin.Core.Dto; using NPP.SmartSchedue.Api.Contracts.Services.Work.Input; using NPP.SmartSchedue.Api.Contracts.Services.Work.Output; using NPP.SmartSchedue.Api.Contracts.Core.Enums; +using NPP.SmartSchedue.Api.Contracts.Domain.Work; namespace NPP.SmartSchedue.Api.Contracts.Services.Work; @@ -112,4 +113,13 @@ public interface IWorkOrderService /// 用户ID /// Task> GetByUserIdAsync(long userId); + + /// + /// 转换任务班次的任务时间 + /// + /// + /// + Task ConvertWorkOrderShift(WorkOrderEntity workOrderEntity); + + Task> GetPendingSubmitCountByCreatorAsync(); } \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Workbench/IWorkbenchService.cs b/NPP.SmartSchedue.Api.Contracts/Services/Workbench/IWorkbenchService.cs new file mode 100644 index 0000000..509206f --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Workbench/IWorkbenchService.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; +using NPP.SmartSchedue.Api.Contracts.Services.Workbench.Input; +using NPP.SmartSchedue.Api.Contracts.Services.Workbench.Output; + +namespace NPP.SmartSchedue.Api.Contracts.Services.Workbench; + +/// +/// 工作台服务接口 +/// 提供工作台相关的数据查询和业务逻辑处理 +/// +public interface IWorkbenchService +{ + /// + /// 获取用户本周日历数据 + /// 包含任务排班信息和班次不可用性信息,用于日历视图展示 + /// + /// 查询参数 + /// 本周日历数据 + Task GetWeeklyCalendarAsync(WeeklyCalendarInput input); +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Workbench/Input/WeeklyCalendarInput.cs b/NPP.SmartSchedue.Api.Contracts/Services/Workbench/Input/WeeklyCalendarInput.cs new file mode 100644 index 0000000..4ee4aff --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Workbench/Input/WeeklyCalendarInput.cs @@ -0,0 +1,31 @@ +using System; + +namespace NPP.SmartSchedue.Api.Contracts.Services.Workbench.Input; + +/// +/// 获取用户本周日历数据输入参数 +/// +public class WeeklyCalendarInput +{ + /// + /// 指定周的日期(可选,默认为当前周) + /// 可以是该周内任意一天,系统会自动计算周的开始和结束日期 + /// + public DateTime? WeekDate { get; set; } + + /// + /// 指定人员ID(可选,默认为当前登录用户) + /// 如果指定则获取该人员的日历数据,否则获取当前登录用户的数据 + /// + public long? PersonnelId { get; set; } + + /// + /// 是否包含已完成的任务(默认true) + /// + public bool IncludeCompletedTasks { get; set; } = true; + + /// + /// 是否包含不可用时间段(默认true) + /// + public bool IncludeUnavailableSlots { get; set; } = true; +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api.Contracts/Services/Workbench/Output/WeeklyCalendarOutput.cs b/NPP.SmartSchedue.Api.Contracts/Services/Workbench/Output/WeeklyCalendarOutput.cs new file mode 100644 index 0000000..de2ebb8 --- /dev/null +++ b/NPP.SmartSchedue.Api.Contracts/Services/Workbench/Output/WeeklyCalendarOutput.cs @@ -0,0 +1,199 @@ +using System; +using System.Collections.Generic; + +namespace NPP.SmartSchedue.Api.Contracts.Services.Workbench.Output; + +/// +/// 用户本周日历数据输出 +/// +public class WeeklyCalendarOutput +{ + /// + /// 周开始日期(周一) + /// + public DateTime WeekStartDate { get; set; } + + /// + /// 周结束日期(周日) + /// + public DateTime WeekEndDate { get; set; } + + /// + /// 人员ID + /// + public long PersonnelId { get; set; } + + /// + /// 人员姓名 + /// + public string PersonnelName { get; set; } + + /// + /// 每日日历数据 + /// + public List Days { get; set; } = new List(); +} + +/// +/// 单日日历数据项 +/// +public class CalendarDayItem +{ + /// + /// 日期 + /// + public DateTime Date { get; set; } + + /// + /// 星期几(1=周一,7=周日) + /// + public int DayOfWeek { get; set; } + + /// + /// 星期几中文名称 + /// + public string DayOfWeekName { get; set; } + + /// + /// 该日的班次数据 + /// + public List Shifts { get; set; } = new List(); +} + +/// +/// 班次日历数据项 +/// +public class CalendarShiftItem +{ + /// + /// 班次ID + /// + public long ShiftId { get; set; } + + /// + /// 班次名称 + /// + public string ShiftName { get; set; } + + /// + /// 班次开始时间 + /// + public TimeSpan StartTime { get; set; } + + /// + /// 班次结束时间 + /// + public TimeSpan EndTime { get; set; } + + /// + /// 该班次的工作任务 + /// + public List Tasks { get; set; } = new List(); + + /// + /// 该班次的不可用时间段 + /// + public List UnavailableItems { get; set; } = new List(); +} + +/// +/// 任务日历项 +/// +public class CalendarTaskItem +{ + /// + /// 任务ID + /// + public long TaskId { get; set; } + + /// + /// 任务代码 + /// + public string TaskCode { get; set; } + + /// + /// 项目号 + /// + public string ProjectNumber { get; set; } + + /// + /// 工序名称 + /// + public string ProcessName { get; set; } + + /// + /// 任务状态 + /// + public int Status { get; set; } + + /// + /// 状态名称 + /// + public string StatusName { get; set; } + + /// + /// 优先级 + /// + public int Priority { get; set; } + + /// + /// 预计工时 + /// + public decimal? EstimatedHours { get; set; } + + /// + /// 计划开始时间 + /// + public DateTime PlannedStartTime { get; set; } + + /// + /// 计划结束时间 + /// + public DateTime PlannedEndTime { get; set; } + + /// + /// 备注 + /// + public string Remarks { get; set; } +} + +/// +/// 不可用时间段项 +/// +public class CalendarUnavailableItem +{ + /// + /// 不可用记录ID + /// + public long UnavailabilityId { get; set; } + + /// + /// 不可用原因类型 + /// + public int ReasonType { get; set; } + + /// + /// 原因类型名称 + /// + public string ReasonTypeName { get; set; } + + /// + /// 备注说明 + /// + public string Remark { get; set; } + + /// + /// 生效开始时间(可选) + /// + public TimeSpan? EffectiveStartTime { get; set; } + + /// + /// 生效结束时间(可选) + /// + public TimeSpan? EffectiveEndTime { get; set; } + + /// + /// 优先级权重 + /// + public int Priority { get; set; } +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api/Core/Consts/CacheKeys.cs b/NPP.SmartSchedue.Api/Core/Consts/CacheKeys.cs index fe9b2da..1b2d384 100644 --- a/NPP.SmartSchedue.Api/Core/Consts/CacheKeys.cs +++ b/NPP.SmartSchedue.Api/Core/Consts/CacheKeys.cs @@ -18,5 +18,29 @@ public static partial class CacheKeys /// 模块Id /// public static string GetModuleActionKey(long id) => $"{ModuleActionKey}{id}"; + + /// + /// 通知设置缓存 + /// + [Description("通知设置缓存")] + public const string NotificationSettingsKey = "notification:settings:list:"; + /// + /// DynamicDepartmentIds + /// + [Description("动态部门人员数据")] public const string DynamicDepartment = "dynamic:department:list:"; + + /// + /// 获取动态部门人员数据 + /// + /// + /// + public static string GetDynamicDepartmentKey(long id) => $"{DynamicDepartment}{id}"; + + + /// + /// 人员缓存 + /// + [Description("人员缓存")] + public const string UserListKey = "user:list:all:"; } \ No newline at end of file diff --git a/NPP.SmartSchedue.Api/NPP.SmartSchedue.Api.csproj b/NPP.SmartSchedue.Api/NPP.SmartSchedue.Api.csproj index 003fb7d..abd644c 100644 --- a/NPP.SmartSchedue.Api/NPP.SmartSchedue.Api.csproj +++ b/NPP.SmartSchedue.Api/NPP.SmartSchedue.Api.csproj @@ -25,7 +25,6 @@ - @@ -34,11 +33,12 @@ - + + diff --git a/NPP.SmartSchedue.Api/Repositories/Notification/NotificationHistoryRepository.cs b/NPP.SmartSchedue.Api/Repositories/Notification/NotificationHistoryRepository.cs index 6f4d4ce..5bc6077 100644 --- a/NPP.SmartSchedue.Api/Repositories/Notification/NotificationHistoryRepository.cs +++ b/NPP.SmartSchedue.Api/Repositories/Notification/NotificationHistoryRepository.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using System.Linq; -using NPP.SmartSchedue.Api.Contracts.Core.Repositories; using NPP.SmartSchedue.Api.Contracts.Domain.Notification; using NPP.SmartSchedue.Api.Contracts.Core.Enums; using NPP.SmartSchedue.Api.Core.Repositories; @@ -166,13 +165,10 @@ public class NotificationHistoryRepository : AppRepositoryBase + // 使用FreeSql的批量更新来提高性能 + foreach (var update in updates) { - foreach (var update in updates) - { - await UpdateSendStatusAsync(update.Id, update.Status, update.Result, update.Error); - } - }); + await UpdateSendStatusAsync(update.Id, update.Status, update.Result, update.Error); + } } } \ No newline at end of file diff --git a/NPP.SmartSchedue.Api/Repositories/Notification/NotificationSettingRepository.cs b/NPP.SmartSchedue.Api/Repositories/Notification/NotificationSettingRepository.cs index 2bf5042..6d48439 100644 --- a/NPP.SmartSchedue.Api/Repositories/Notification/NotificationSettingRepository.cs +++ b/NPP.SmartSchedue.Api/Repositories/Notification/NotificationSettingRepository.cs @@ -3,7 +3,6 @@ using System.Collections.Generic; using System.Threading.Tasks; using System.Linq; using Newtonsoft.Json; -using NPP.SmartSchedue.Api.Contracts.Core.Repositories; using NPP.SmartSchedue.Api.Contracts.Domain.Notification; using NPP.SmartSchedue.Api.Core.Repositories; using ZhonTai.Admin.Core.Db.Transaction; @@ -128,12 +127,48 @@ public class NotificationSettingRepository : AppRepositoryBase public async Task> GetActiveNotificationSettingsForTimeAsync(DateTime currentTime) { - var currentTimeString = currentTime.ToString("HH:mm"); - - return await Select + var allEnabledSettings = await Select .Where(n => n.IsEnabled == true) - .Where(n => n.StartTime <= currentTimeString && n.EndTime >= currentTimeString) + .Where(n => !string.IsNullOrWhiteSpace(n.StartTime) && !string.IsNullOrWhiteSpace(n.EndTime)) .Include(n => n.PersonnelGroup) .ToListAsync(); + + var activeSettings = new List(); + var currentTimeOnly = TimeOnly.FromDateTime(currentTime); + + foreach (var setting in allEnabledSettings) + { + try + { + if (TimeOnly.TryParseExact(setting.StartTime, "HH:mm", out var startTime) && + TimeOnly.TryParseExact(setting.EndTime, "HH:mm", out var endTime)) + { + // 处理跨天的情况(如:22:00-06:00) + if (startTime <= endTime) + { + // 同一天内的时间段 + if (currentTimeOnly >= startTime && currentTimeOnly <= endTime) + { + activeSettings.Add(setting); + } + } + else + { + // 跨天的时间段 + if (currentTimeOnly >= startTime || currentTimeOnly <= endTime) + { + activeSettings.Add(setting); + } + } + } + } + catch + { + // 时间格式解析失败,跳过此设置 + continue; + } + } + + return activeSettings; } } \ No newline at end of file diff --git a/NPP.SmartSchedue.Api/Repositories/Notification/PersonnelGroupRepository.cs b/NPP.SmartSchedue.Api/Repositories/Notification/PersonnelGroupRepository.cs index 7aba444..56c996a 100644 --- a/NPP.SmartSchedue.Api/Repositories/Notification/PersonnelGroupRepository.cs +++ b/NPP.SmartSchedue.Api/Repositories/Notification/PersonnelGroupRepository.cs @@ -2,7 +2,6 @@ using System.Collections.Generic; using System.Threading.Tasks; using System.Linq; using Newtonsoft.Json; -using NPP.SmartSchedue.Api.Contracts.Core.Repositories; using NPP.SmartSchedue.Api.Contracts.Domain.Notification; using NPP.SmartSchedue.Api.Contracts.Core.Enums; using NPP.SmartSchedue.Api.Core.Repositories; diff --git a/NPP.SmartSchedue.Api/ServiceCollectionExtensions.cs b/NPP.SmartSchedue.Api/ServiceCollectionExtensions.cs index 6e82eaa..dd1ff01 100644 --- a/NPP.SmartSchedue.Api/ServiceCollectionExtensions.cs +++ b/NPP.SmartSchedue.Api/ServiceCollectionExtensions.cs @@ -9,6 +9,10 @@ using NPP.SmartSchedue.Api.Services.Equipment; using NPP.SmartSchedue.Api.Contracts.Domain.Equipment; using NPP.SmartSchedue.Api.Repositories.Equipment; using NPP.SmartSchedue.Api.Repositories.Personnel; +using NPP.SmartSchedue.Api.Contracts.Services.Notification; +using NPP.SmartSchedue.Api.Services.Notification; +using NPP.SmartSchedue.Api.Contracts.Domain.Notification; +using NPP.SmartSchedue.Api.Repositories.Notification; using NPP.SmartSchedue.Api.Contracts; using NPP.SmartSchedue.Api.Services.Integration.Algorithms; using System; @@ -43,6 +47,7 @@ namespace NPP.SmartSchedue.Api // 注册人员资质Repository services.AddScoped(); + services.AddScoped(); return services; } @@ -62,5 +67,38 @@ namespace NPP.SmartSchedue.Api return services; } + + /// + /// 注册通知相关服务 + /// + public static IServiceCollection AddNotificationServices(this IServiceCollection services) + { + // 注册通知相关Repository + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + // 注册通知相关Service + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + services.AddScoped(); + + return services; + } + + /// + /// 注册工作订单整合操作相关服务 + /// + public static IServiceCollection AddWorkOrderIntegrationOperationServices(this IServiceCollection services) + { + // 注册状态验证器 + services.AddScoped(); + + // 注册统一操作服务接口和实现 + services.AddScoped(); + + return services; + } } } \ No newline at end of file diff --git a/NPP.SmartSchedue.Api/Services/Integration/Algorithms/ContextBuilderEngine.cs b/NPP.SmartSchedue.Api/Services/Integration/Algorithms/ContextBuilderEngine.cs index 4be328d..2cb4e2d 100644 --- a/NPP.SmartSchedue.Api/Services/Integration/Algorithms/ContextBuilderEngine.cs +++ b/NPP.SmartSchedue.Api/Services/Integration/Algorithms/ContextBuilderEngine.cs @@ -7,6 +7,7 @@ using System.Threading; using System.Threading.Tasks; using FreeSql; using Microsoft.Extensions.Logging; +using NPP.SmartSchedue.Api.Contracts.Core.Enums; using NPP.SmartSchedue.Api.Contracts.Domain.Time; using NPP.SmartSchedue.Api.Contracts.Domain.Work; using NPP.SmartSchedue.Api.Contracts.Services.Integration.Input; @@ -497,21 +498,14 @@ namespace NPP.SmartSchedue.Api.Services.Integration.Algorithms try { _logger.LogDebug("📚 开始加载人员历史任务数据"); - - var personnelIds = context.AvailablePersonnel?.Select(p => p.Id).ToList() ?? new List(); - if (!personnelIds.Any()) - { - _logger.LogDebug("⏭️ 跳过历史任务加载,无人员数据"); - return; - } - // 查询最近的历史任务(例如最近30天) var recentDate = DateTime.Now.AddDays(-30); var historyTasks = await _workOrderRepository.Select .Where(w => w.WorkOrderDate >= recentDate && w.AssignedPersonnelId.HasValue && - personnelIds.Contains(w.AssignedPersonnelId.Value)) + w.Status >= (int)WorkOrderStatusEnum.Assigned && + w.Status < (int)WorkOrderStatusEnum.Cancelled) .ToListAsync(); // 按人员分组 @@ -861,7 +855,6 @@ namespace NPP.SmartSchedue.Api.Services.Integration.Algorithms // 通过所有筛选条件 result.IsFeasible = true; - context.PrefilterResults[key] = result; } } @@ -1143,8 +1136,7 @@ namespace NPP.SmartSchedue.Api.Services.Integration.Algorithms if (context.PersonnelHistoryTasks.TryGetValue(personnel.Id, out var historyTasks)) { var conflictingTasks = historyTasks.Where(h => - h.WorkDate.Date == task.WorkOrderDate.Date && - h.ShiftId == task.ShiftId).ToList(); + h.WorkDate.Date == task.WorkOrderDate.Date).ToList(); if (conflictingTasks.Any()) return false; diff --git a/NPP.SmartSchedue.Api/Services/Integration/Algorithms/LinearProgrammingEngine.cs b/NPP.SmartSchedue.Api/Services/Integration/Algorithms/LinearProgrammingEngine.cs index 12b3c6c..ad3c992 100644 --- a/NPP.SmartSchedue.Api/Services/Integration/Algorithms/LinearProgrammingEngine.cs +++ b/NPP.SmartSchedue.Api/Services/Integration/Algorithms/LinearProgrammingEngine.cs @@ -54,7 +54,7 @@ namespace NPP.SmartSchedue.Api.Services.Integration.Algorithms try { - _logger.LogInformation("🚀 启动增强版线性规划优化 - 任务数:{TaskCount},人员数:{PersonnelCount},预筛选缓存:{CacheSize}", + _logger.LogDebug("🚀 启动增强版线性规划优化 - 任务数:{TaskCount},人员数:{PersonnelCount},预筛选缓存:{CacheSize}", context.Tasks.Count, context.AvailablePersonnel.Count, context.PrefilterResults.Count); // 第一步:创建高性能求解器 @@ -62,11 +62,11 @@ namespace NPP.SmartSchedue.Api.Services.Integration.Algorithms // 第二步:构建决策变量矩阵 (x[i,j] = 1表示任务i分配给人员j) var decisionVariables = BuildDecisionVariables(solver, context); - _logger.LogInformation("📊 决策变量构建完成 - 变量数量:{VariableCount}", decisionVariables.Count); + _logger.LogDebug("📊 决策变量构建完成 - 变量数量:{VariableCount}", decisionVariables.Count); // 第三步:构建辅助变量 (支持复杂约束建模) var auxiliaryVariables = BuildAuxiliaryVariables(solver, context, decisionVariables); - _logger.LogInformation("🔧 辅助变量构建完成 - 辅助变量数量:{AuxCount}", auxiliaryVariables.Count); + _logger.LogDebug("🔧 辅助变量构建完成 - 辅助变量数量:{AuxCount}", auxiliaryVariables.Count); // 第四步:Level 1 基础约束 - 对应遗传算法的基础约束验证 await AddLevel1BasicConstraints(solver, decisionVariables, auxiliaryVariables, context); @@ -157,7 +157,7 @@ namespace NPP.SmartSchedue.Api.Services.Integration.Algorithms { unavailabilitySkippedCount++; skippedCount++; - _logger.LogTrace("🚫 跳过不可用组合 - 任务{TaskId}人员{PersonnelId}在{Date}班次{ShiftId}不可用", + _logger.LogDebug("🚫 跳过不可用组合 - 任务{TaskId}人员{PersonnelId}在{Date}班次{ShiftId}不可用", task.Id, personnel.Id, task.WorkOrderDate.ToString("yyyy-MM-dd"), task.ShiftId); continue; } @@ -190,7 +190,7 @@ namespace NPP.SmartSchedue.Api.Services.Integration.Algorithms } } - _logger.LogInformation("📊 【增强版】决策变量构建统计 - 创建:{Created}个,跳过:{Skipped}个(不可用性:{UnavailabilitySkipped}),变量密度:{Density:P2},不可用性优化率:{UnavailabilityRate:P2}", + _logger.LogDebug("📊 【增强版】决策变量构建统计 - 创建:{Created}个,跳过:{Skipped}个(不可用性:{UnavailabilitySkipped}),变量密度:{Density:P2},不可用性优化率:{UnavailabilityRate:P2}", createdCount, skippedCount, unavailabilitySkippedCount, (double)createdCount / (createdCount + skippedCount), (double)unavailabilitySkippedCount / (createdCount + skippedCount)); @@ -229,15 +229,15 @@ namespace NPP.SmartSchedue.Api.Services.Integration.Algorithms } // 3. 时间冲突指示变量 (二进制变量,标识是否存在时间冲突) - var timeSlots = context.Tasks.Select(t => new { Date = t.WorkOrderDate.Date, ShiftId = t.ShiftId }) + var timeSlots = context.Tasks.Select(t => new { Date = t.WorkOrderDate.Date }) .Distinct().ToList(); foreach (var personnel in context.AvailablePersonnel) { foreach (var timeSlot in timeSlots) { - var conflictVar = solver.MakeBoolVar($"conflict_{personnel.Id}_{timeSlot.Date:yyyyMMdd}_{timeSlot.ShiftId}"); - auxiliaryVars[$"conflict_{personnel.Id}_{timeSlot.Date:yyyyMMdd}_{timeSlot.ShiftId}"] = conflictVar; + var conflictVar = solver.MakeBoolVar($"conflict_{personnel.Id}_{timeSlot.Date:yyyyMMdd}"); + auxiliaryVars[$"conflict_{personnel.Id}_{timeSlot.Date:yyyyMMdd}"] = conflictVar; } } @@ -256,9 +256,9 @@ namespace NPP.SmartSchedue.Api.Services.Integration.Algorithms auxiliaryVars[$"fl_violation_{task.Id}"] = flViolationVar; }*/ - _logger.LogInformation("🔧 辅助变量构建完成 - 总数:{AuxVarCount}(工作负载:{WorkloadCount},冲突指示:{ConflictCount},班次违规:{ShiftCount},FL违规:{FLCount})", + _logger.LogDebug("🔧 辅助变量构建完成 - 总数:{AuxVarCount}(工作负载:{WorkloadCount},冲突指示:{ConflictCount},班次违规:{ShiftCount},FL违规:{FLCount})", auxiliaryVars.Count, - context.AvailablePersonnel.Count, + context.Tasks.Count / context.AvailablePersonnel.Count, context.AvailablePersonnel.Count * timeSlots.Count, context.AvailablePersonnel.Count, context.Tasks.Count); @@ -284,7 +284,7 @@ namespace NPP.SmartSchedue.Api.Services.Integration.Algorithms await Task.CompletedTask; var constraintCount = 0; - _logger.LogInformation("🏗️ 开始添加Level 1基础约束..."); + _logger.LogDebug("🏗️ 开始添加Level 1基础约束..."); // 基础约束1:任务分配完整性 - 每个任务必须且只能分配给一个人员 foreach (var task in context.Tasks) @@ -306,7 +306,7 @@ namespace NPP.SmartSchedue.Api.Services.Integration.Algorithms foreach (var personnel in context.AvailablePersonnel) { // 按时间段分组检查冲突 - var timeGroups = context.Tasks.GroupBy(t => new { Date = t.WorkOrderDate.Date, ShiftId = t.ShiftId }); + var timeGroups = context.Tasks.GroupBy(t => new { Date = t.WorkOrderDate.Date }); foreach (var timeGroup in timeGroups) { @@ -322,7 +322,7 @@ namespace NPP.SmartSchedue.Api.Services.Integration.Algorithms { // 约束:同一人员在同一时间段最多执行1个任务 var constraint = solver.MakeConstraint(0, 1, - $"time_conflict_{personnel.Id}_{timeGroup.Key.Date:yyyyMMdd}_{timeGroup.Key.ShiftId}"); + $"time_conflict_{personnel.Id}_{timeGroup.Key.Date:yyyyMMdd}"); foreach (var variable in timeSlotVariables) { @@ -331,12 +331,12 @@ namespace NPP.SmartSchedue.Api.Services.Integration.Algorithms constraintCount++; // 连接辅助变量(用于目标函数惩罚) - var conflictVarKey = $"conflict_{personnel.Id}_{timeGroup.Key.Date:yyyyMMdd}_{timeGroup.Key.ShiftId}"; + var conflictVarKey = $"conflict_{personnel.Id}_{timeGroup.Key.Date:yyyyMMdd}"; if (auxiliaryVars.ContainsKey(conflictVarKey)) { // 如果分配了多个任务,冲突变量=1 var conflictConstraint = solver.MakeConstraint(0, double.PositiveInfinity, - $"conflict_indicator_{personnel.Id}_{timeGroup.Key.Date:yyyyMMdd}_{timeGroup.Key.ShiftId}"); + $"conflict_indicator_{personnel.Id}_{timeGroup.Key.Date:yyyyMMdd}"); foreach (var variable in timeSlotVariables) { @@ -367,7 +367,7 @@ namespace NPP.SmartSchedue.Api.Services.Integration.Algorithms } } - _logger.LogInformation("✅ Level 1基础约束添加完成 - 约束数量:{ConstraintCount}", constraintCount); + _logger.LogDebug("✅ Level 1基础约束添加完成 - 约束数量:{ConstraintCount}", constraintCount); } #endregion @@ -387,15 +387,15 @@ namespace NPP.SmartSchedue.Api.Services.Integration.Algorithms { var constraintCount = 0; - _logger.LogInformation("🔄 开始添加Level 2组合约束..."); + _logger.LogDebug("🔄 开始添加Level 2组合约束..."); // 组合约束1:动态班次规则约束 - 对应CalculateDynamicShiftRuleScore - constraintCount += await AddDynamicShiftRuleConstraints(solver, decisionVars, auxiliaryVars, context); + //constraintCount += await AddDynamicShiftRuleConstraints(solver, decisionVars, auxiliaryVars, context); // 组合约束3:人员日工作量限制约束 - constraintCount += AddDailyWorkloadConstraints(solver, decisionVars, auxiliaryVars, context); + //constraintCount += AddDailyWorkloadConstraints(solver, decisionVars, auxiliaryVars, context); - _logger.LogInformation("✅ Level 2组合约束添加完成 - 约束数量:{ConstraintCount}", constraintCount); + _logger.LogDebug("✅ Level 2组合约束添加完成 - 约束数量:{ConstraintCount}", constraintCount); } /// @@ -582,7 +582,7 @@ namespace NPP.SmartSchedue.Api.Services.Integration.Algorithms await Task.CompletedTask; var constraintCount = 0; - _logger.LogInformation("💼 开始添加Level 3业务约束..."); + _logger.LogDebug("💼 开始添加Level 3业务约束..."); // 业务约束1:FL优先级规则 - 对应CalculateFLPriorityRuleScore //constraintCount += AddFLPriorityConstraints(solver, decisionVars, auxiliaryVars, context); @@ -594,12 +594,12 @@ namespace NPP.SmartSchedue.Api.Services.Integration.Algorithms //constraintCount += AddProjectContinuityConstraints(solver, decisionVars, auxiliaryVars, context); // 业务约束4:人员资质约束 - 确保只分配给具备相应资质的人员 - constraintCount += await AddQualificationConstraints(solver, decisionVars, context); + //constraintCount += await AddQualificationConstraints(solver, decisionVars, context); // 业务约束5:【Ultra think】人员班次不可用性硬约束 - 确保不会分配给不可用人员 - constraintCount += await AddPersonnelUnavailabilityConstraints(solver, decisionVars, context); + //constraintCount += await AddPersonnelUnavailabilityConstraints(solver, decisionVars, context); - _logger.LogInformation("✅ Level 3业务约束添加完成 - 约束数量:{ConstraintCount}", constraintCount); + _logger.LogDebug("✅ Level 3业务约束添加完成 - 约束数量:{ConstraintCount}", constraintCount); } /// @@ -916,7 +916,7 @@ namespace NPP.SmartSchedue.Api.Services.Integration.Algorithms } constraintCount++; - _logger.LogInformation("✅ 公平性约束添加完成 - 约束数量:{ConstraintCount},平均负载:{AvgWorkload:F2},最大负载:{MaxWorkload:F2},最小参与人数:{MinParticipation}", + _logger.LogDebug("✅ 公平性约束添加完成 - 约束数量:{ConstraintCount},平均负载:{AvgWorkload:F2},最大负载:{MaxWorkload:F2},最小参与人数:{MinParticipation}", constraintCount, avgWorkload, maxReasonableWorkload, minPersonnelUtilization); } @@ -950,7 +950,7 @@ namespace NPP.SmartSchedue.Api.Services.Integration.Algorithms AddFairnessRewards(objective, auxiliaryVars, context); // 目标组件4:业务优先级奖励 - 对应遗传算法的业务逻辑优化 - AddBusinessPriorityRewards(objective, decisionVars, auxiliaryVars, context); + //AddBusinessPriorityRewards(objective, decisionVars, auxiliaryVars, context); _logger.LogInformation("✅ 增强版目标函数构建完成,包含4大评分维度"); } @@ -1004,16 +1004,16 @@ namespace NPP.SmartSchedue.Api.Services.Integration.Algorithms } // Level 2惩罚:班次规则违规重度惩罚 - foreach (var kvp in auxiliaryVars.Where(v => v.Key.StartsWith("shift_violation_"))) - { - objective.SetCoefficient(kvp.Value, -LEVEL2_CONSTRAINT_PENALTY); - } + // foreach (var kvp in auxiliaryVars.Where(v => v.Key.StartsWith("shift_violation_"))) + // { + // objective.SetCoefficient(kvp.Value, -LEVEL2_CONSTRAINT_PENALTY); + // } // Level 3惩罚:业务逻辑违规中度惩罚 - foreach (var kvp in auxiliaryVars.Where(v => v.Key.StartsWith("fl_violation_"))) - { - objective.SetCoefficient(kvp.Value, -LEVEL3_CONSTRAINT_PENALTY); - } + // foreach (var kvp in auxiliaryVars.Where(v => v.Key.StartsWith("fl_violation_"))) + // { + // objective.SetCoefficient(kvp.Value, -LEVEL3_CONSTRAINT_PENALTY); + // } } /// @@ -1025,11 +1025,11 @@ namespace NPP.SmartSchedue.Api.Services.Integration.Algorithms GlobalAllocationContext context) { // 负载均衡奖励:减少偏差获得奖励 - foreach (var kvp in auxiliaryVars.Where(v => v.Key.StartsWith("pos_dev_") || v.Key.StartsWith("neg_dev_"))) - { - // 偏差越小,奖励越多(通过负的偏差系数实现) - objective.SetCoefficient(kvp.Value, -FAIRNESS_REWARD_FACTOR); - } + // foreach (var kvp in auxiliaryVars.Where(v => v.Key.StartsWith("pos_dev_") || v.Key.StartsWith("neg_dev_"))) + // { + // // 偏差越小,奖励越多(通过负的偏差系数实现) + // objective.SetCoefficient(kvp.Value, -FAIRNESS_REWARD_FACTOR); + // } } /// @@ -2282,7 +2282,18 @@ namespace NPP.SmartSchedue.Api.Services.Integration.Algorithms } } } + + // 检查历史任务人员的分配 + if (context.PersonnelHistoryTasks.TryGetValue(personnelId, out var historyTask)) + { + var taskDate = task.WorkOrderDate.Date; + if (historyTask.Any(m => m.WorkDate.Date == taskDate)) + { + return true; + } + } + return false; // 人员可用 } catch (Exception ex) diff --git a/NPP.SmartSchedue.Api/Services/Integration/IntegrationRecordService.cs b/NPP.SmartSchedue.Api/Services/Integration/IntegrationRecordService.cs index c84e7c6..8afdcb6 100644 --- a/NPP.SmartSchedue.Api/Services/Integration/IntegrationRecordService.cs +++ b/NPP.SmartSchedue.Api/Services/Integration/IntegrationRecordService.cs @@ -9,6 +9,7 @@ using NPP.SmartSchedue.Api.Contracts.Services.Integration; using NPP.SmartSchedue.Api.Contracts.Services.Integration.Input; using NPP.SmartSchedue.Api.Contracts.Services.Integration.Output; using NPP.SmartSchedue.Api.Repositories.Work; +using NPP.SmartSchedue.Api.Repositories.Equipment; using System; using System.Collections.Generic; using System.Linq; @@ -28,15 +29,18 @@ namespace NPP.SmartSchedue.Api.Services.Integration { private readonly IIntegrationRecordRepository _integrationRecordRepository; private readonly WorkOrderRepository _workOrderRepository; + private readonly EquipmentRepository _equipmentRepository; private readonly ILogger _logger; public IntegrationRecordService( IIntegrationRecordRepository integrationRecordRepository, WorkOrderRepository workOrderRepository, + EquipmentRepository equipmentRepository, ILogger logger) { _integrationRecordRepository = integrationRecordRepository; _workOrderRepository = workOrderRepository; + _equipmentRepository = equipmentRepository; _logger = logger; } @@ -922,6 +926,112 @@ namespace NPP.SmartSchedue.Api.Services.Integration #endregion + #region 任务修改接口实现 + + /// + /// 修改整合记录中的任务分配 + /// 根据选中的人员修改任务的指定人员ID和姓名,并更新整合记录 + /// + [HttpPost] + public async Task ModifyIntegrationRecordTaskAsync(IntegrationRecordTaskModifyInput input) + { + try + { + _logger.LogInformation("开始修改整合记录任务: RecordId={RecordId}, ModificationsCount={Count}", + input.IntegrationRecordId, input.TaskModifications.Count); + + // 获取整合记录 + var integrationRecord = await _integrationRecordRepository.GetAsync(input.IntegrationRecordId); + if (integrationRecord == null) + { + _logger.LogWarning("未找到整合记录: RecordId={RecordId}", input.IntegrationRecordId); + return false; + } + + // 获取需要修改的任务 + var taskIds = input.TaskModifications.Select(m => m.TaskId).ToList(); + var tasks = await _workOrderRepository.Select + .Where(t => taskIds.Contains(t.Id)) + .ToListAsync(); + + if (!tasks.Any()) + { + _logger.LogWarning("未找到需要修改的任务: RecordId={RecordId}", input.IntegrationRecordId); + return false; + } + + // 执行任务修改 + foreach (var modification in input.TaskModifications) + { + var task = tasks.FirstOrDefault(t => t.Id == modification.TaskId); + if (task == null) continue; + + _logger.LogInformation("修改任务分配: TaskId={TaskId}, TaskCode={TaskCode}", + modification.TaskId, modification.TaskCode ?? "N/A"); + + // 修改人员分配 + if (modification.NewPersonnelId.HasValue) + { + task.AssignedPersonnelId = modification.NewPersonnelId.Value; + task.AssignedPersonnelName = modification.NewPersonnelName ?? string.Empty; + _logger.LogInformation(" 更新人员分配: PersonnelId={PersonnelId}, PersonnelName={PersonnelName}", + modification.NewPersonnelId.Value, modification.NewPersonnelName ?? "N/A"); + } + + // 修改设备分配 + if (modification.NewEquipmentId.HasValue) + { + task.AssignedEquipmentId = modification.NewEquipmentId.Value; + task.AssignedEquipmentName = modification.NewEquipmentName ?? string.Empty; + _logger.LogInformation(" 更新设备分配: EquipmentId={EquipmentId}, EquipmentName={EquipmentName}", + modification.NewEquipmentId.Value, modification.NewEquipmentName ?? "N/A"); + } + + // 更新任务备注 + if (!string.IsNullOrWhiteSpace(modification.ModificationNote)) + { + task.Remarks = string.IsNullOrWhiteSpace(task.Remarks) + ? $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {modification.ModificationNote}" + : $"{task.Remarks}\n[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {modification.ModificationNote}"; + } + + task.ModifiedTime = DateTime.Now; + task.ModifiedUserId = input.OperatorUserId; + task.ModifiedUserName = input.OperatorUserName; + } + + // 批量更新任务 + await _workOrderRepository.UpdateAsync(tasks); + + // 更新整合记录备注 + if (!string.IsNullOrWhiteSpace(input.ModificationReason)) + { + integrationRecord.Remarks = string.IsNullOrWhiteSpace(integrationRecord.Remarks) + ? $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {input.ModificationReason}" + : $"{integrationRecord.Remarks}\n[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {input.ModificationReason}"; + + integrationRecord.ModifiedTime = DateTime.Now; + integrationRecord.ModifiedUserId = input.OperatorUserId; + integrationRecord.ModifiedUserName = input.OperatorUserName; + + await _integrationRecordRepository.UpdateAsync(integrationRecord); + } + + _logger.LogInformation("整合记录任务修改完成: RecordId={RecordId}, SuccessCount={Count}", + input.IntegrationRecordId, input.TaskModifications.Count); + + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "修改整合记录任务失败: RecordId={RecordId}, Error={Message}", + input.IntegrationRecordId, ex.Message); + return false; + } + } + + #endregion + #region 通知管理接口实现(预留) /// @@ -929,10 +1039,10 @@ namespace NPP.SmartSchedue.Api.Services.Integration /// 深度业务思考:为未来的通知系统集成预留完整的接口结构 /// [HttpPost] - public async Task SendPublishNotificationAsync(long recordId, string notificationType, List targetPersonnelIds, string customMessage = "") + public async Task SendPublishNotificationAsync(long recordId, string notificationType, List targetPersonnelIds, string customMessage = "") { // 预留实现:当前返回模拟结果,未来集成真实通知服务 - var result = new NotificationSendResult + var result = new IntegrationNotificationResult { NotificationType = notificationType, IsSent = true, // 预留:假设发送成功 @@ -1056,6 +1166,11 @@ namespace NPP.SmartSchedue.Api.Services.Integration /// /// 执行任务发布 + /// 业务思考: + /// 1. 批量处理任务发布,但每个任务独立处理避免相互影响 + /// 2. 详细记录发布过程和结果,便于问题排查和审计 + /// 3. 支持部分成功的发布场景,返回成功和失败的详细信息 + /// 4. 在发布失败时提供具体的失败原因和建议 /// private async Task<(List PublishedTasks, List FailedPublishTasks)> ExecuteTaskPublishAsync( List tasks, @@ -1065,18 +1180,29 @@ namespace NPP.SmartSchedue.Api.Services.Integration var publishedTasks = new List(); var failedTasks = new List(); + _logger.LogInformation("开始执行任务批量发布: RecordId={RecordId}, TaskCount={Count}, AutoStart={AutoStart}", + integrationRecord.Id, tasks.Count, input.AutoStartTasksAfterPublish); + foreach (var task in tasks) { try { // 记录发布前状态 - var previousStatus = task.Status.ToString(); + var previousStatus = (WorkOrderStatusEnum)task.Status; + var previousStatusName = previousStatus.ToString(); + + _logger.LogDebug("开始发布任务: TaskId={TaskId}, TaskCode={Code}, CurrentStatus={Status}", + task.Id, task.WorkOrderCode, previousStatusName); // 执行任务发布逻辑 var publishSuccess = await PublishSingleTaskAsync(task, integrationRecord, input); if (publishSuccess) { + // 重新获取更新后的状态 + var currentStatus = (WorkOrderStatusEnum)task.Status; + var currentStatusName = currentStatus.ToString(); + publishedTasks.Add(new PublishedTaskDetail { TaskId = task.Id, @@ -1085,20 +1211,44 @@ namespace NPP.SmartSchedue.Api.Services.Integration AssignedPersonnelName = task.AssignedPersonnelName ?? "", AssignedEquipmentId = task.AssignedEquipmentId, AssignedEquipmentName = task.AssignedEquipmentName ?? "", - PreviousStatus = previousStatus, - CurrentStatus = task.Status.ToString(), + PreviousStatus = previousStatusName, + CurrentStatus = currentStatusName, PublishTime = DateTime.Now }); + + _logger.LogInformation("任务发布成功: TaskId={TaskId}, StatusChange={Previous}->{Current}", + task.Id, previousStatusName, currentStatusName); } else { + // 分析发布失败的具体原因 + var failureReasons = new List(); + + if (previousStatus != WorkOrderStatusEnum.PendingIntegration) + { + failureReasons.Add($"任务状态不正确,当前状态:{previousStatusName},期望状态:PendingIntegration"); + } + + if (task.AssignedPersonnelId == null || task.AssignedPersonnelId <= 0) + { + failureReasons.Add("任务未分配人员"); + } + + if (string.IsNullOrEmpty(task.WorkOrderCode)) + { + failureReasons.Add("任务编号为空"); + } + failedTasks.Add(new FailedPublishTaskDetail { TaskId = task.Id, TaskCode = task.WorkOrderCode ?? $"TASK-{task.Id}", FailureReason = "任务发布失败", - DetailedErrors = new List { "发布过程中遇到业务规则限制" } + DetailedErrors = failureReasons.Any() ? failureReasons : new List { "发布过程中遇到未知问题" } }); + + _logger.LogWarning("任务发布失败: TaskId={TaskId}, Reasons={Reasons}", + task.Id, string.Join("; ", failureReasons)); } } catch (Exception ex) @@ -1108,36 +1258,95 @@ namespace NPP.SmartSchedue.Api.Services.Integration TaskId = task.Id, TaskCode = task.WorkOrderCode ?? $"TASK-{task.Id}", FailureReason = "发布异常", - DetailedErrors = new List { ex.Message } + DetailedErrors = new List { ex.Message, ex.StackTrace ?? "" } }); - _logger.LogError(ex, "任务发布失败: TaskId={TaskId}, Error={Message}", task.Id, ex.Message); + _logger.LogError(ex, "任务发布异常: TaskId={TaskId}, TaskCode={Code}, Error={Message}", + task.Id, task.WorkOrderCode, ex.Message); } } + _logger.LogInformation("任务批量发布完成: RecordId={RecordId}, Success={Success}, Failed={Failed}", + integrationRecord.Id, publishedTasks.Count, failedTasks.Count); + return (publishedTasks, failedTasks); } /// /// 发布单个任务 + /// 深度业务思考: + /// 1. 验证任务状态转换的合法性(只有PendingIntegration状态的任务可以发布) + /// 2. 支持AutoStartTasksAfterPublish选项,发布后可直接开始任务 + /// 3. 完整的异常处理和日志记录 + /// 4. 原子性操作确保数据一致性 /// private async Task PublishSingleTaskAsync(WorkOrderEntity task, IntegrationRecordEntity integrationRecord, PublishIntegrationRecordInput input) { try { - // 更新任务状态为已分配(发布后的任务状态) - task.Status = (int)WorkOrderStatusEnum.Assigned; + // 验证任务当前状态是否可以发布 + var currentStatus = (WorkOrderStatusEnum)task.Status; + if (currentStatus != WorkOrderStatusEnum.PendingIntegration) + { + _logger.LogWarning("任务状态不允许发布: TaskId={TaskId}, CurrentStatus={Status}", + task.Id, currentStatus); + return false; + } + + // 确定发布后的目标状态 + WorkOrderStatusEnum targetStatus; + if (input.AutoStartTasksAfterPublish) + { + // 自动开始任务:PendingIntegration -> Assigned -> InProgress + targetStatus = WorkOrderStatusEnum.InProgress; + _logger.LogInformation("任务将发布并自动开始: TaskId={TaskId}", task.Id); + } + else + { + // 常规发布:PendingIntegration -> Assigned + targetStatus = WorkOrderStatusEnum.Assigned; + _logger.LogInformation("任务将发布为已分配状态: TaskId={TaskId}", task.Id); + } + + // 更新任务状态和相关字段 + task.Status = (int)targetStatus; task.ModifiedTime = DateTime.Now; task.ModifiedUserId = input.PublishedByUserId; task.ModifiedUserName = input.PublishedByUserName; + + // 如果是自动开始任务,设置开始时间 + if (input.AutoStartTasksAfterPublish) + { + task.ActualStartTime = DateTime.Now; + } - // 保存任务更新 + // 设置发布时间(如果是定时发布) + if (input.ScheduledPublishTime.HasValue) + { + task.PlannedStartTime = input.ScheduledPublishTime.Value; + } + + // 保存任务更新(原子操作) await _workOrderRepository.UpdateAsync(task); + // 验证更新结果 + var updatedTask = await _workOrderRepository.GetAsync(task.Id); + if (updatedTask == null || updatedTask.Status != (int)targetStatus) + { + _logger.LogError("任务状态更新验证失败: TaskId={TaskId}, ExpectedStatus={Expected}, ActualStatus={Actual}", + task.Id, targetStatus, updatedTask?.Status); + return false; + } + + _logger.LogInformation("任务发布成功: TaskId={TaskId}, OldStatus={OldStatus}, NewStatus={NewStatus}", + task.Id, currentStatus, targetStatus); + return true; } - catch + catch (Exception ex) { + _logger.LogError(ex, "任务发布失败: TaskId={TaskId}, IntegrationRecordId={RecordId}, Error={Message}", + task.Id, integrationRecord.Id, ex.Message); return false; } } @@ -1164,9 +1373,9 @@ namespace NPP.SmartSchedue.Api.Services.Integration /// /// 发送发布通知 /// - private async Task> SendPublishNotificationsAsync(IntegrationRecordEntity integrationRecord, PublishIntegrationRecordInput input) + private async Task> SendPublishNotificationsAsync(IntegrationRecordEntity integrationRecord, PublishIntegrationRecordInput input) { - var results = new List(); + var results = new List(); // 预留:根据发布规则确定通知对象和内容 var targetPersonnelIds = new List(); // 从业务规则中获取 diff --git a/NPP.SmartSchedue.Api/Services/Integration/SmartScheduleOrchestratorService.cs b/NPP.SmartSchedue.Api/Services/Integration/SmartScheduleOrchestratorService.cs index 3a0ddd3..f3652df 100644 --- a/NPP.SmartSchedue.Api/Services/Integration/SmartScheduleOrchestratorService.cs +++ b/NPP.SmartSchedue.Api/Services/Integration/SmartScheduleOrchestratorService.cs @@ -1057,6 +1057,17 @@ namespace NPP.SmartSchedue.Api.Services.Integration return await _integrationRecordService.PublishIntegrationRecordAsync(input); } + /// + /// 修改整合记录中的任务分配 + /// 根据选中的人员修改任务的指定人员ID和姓名,并更新整合记录 + /// + /// 任务修改输入参数 + /// 修改结果 + [HttpPost] + public async Task ModifyIntegrationRecordTaskAsync(IntegrationRecordTaskModifyInput input) + { + return await _integrationRecordService.ModifyIntegrationRecordTaskAsync(input); + } /// /// 转换全局分配结果为人员分配结果 diff --git a/NPP.SmartSchedue.Api/Services/Integration/TaskAssignmentModificationService.cs b/NPP.SmartSchedue.Api/Services/Integration/TaskAssignmentModificationService.cs new file mode 100644 index 0000000..59317e9 --- /dev/null +++ b/NPP.SmartSchedue.Api/Services/Integration/TaskAssignmentModificationService.cs @@ -0,0 +1,1123 @@ +using FreeSql; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using NPP.SmartSchedue.Api.Contracts.Core.Enums; +using NPP.SmartSchedue.Api.Contracts.Domain.Integration; +using NPP.SmartSchedue.Api.Contracts.Domain.Work; +using NPP.SmartSchedue.Api.Contracts.Services.Integration.Input; +using NPP.SmartSchedue.Api.Contracts.Services.Integration.Output; +using NPP.SmartSchedue.Api.Repositories.Work; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using ZhonTai.Admin.Services; +using ZhonTai.DynamicApi; +using ZhonTai.DynamicApi.Attributes; + +namespace NPP.SmartSchedue.Api.Services.Integration +{ + /// + /// 任务分配修改服务 + /// 专门处理任务人员分配的修改操作 + /// 深度业务思考:实现分配结果的精确修改,确保数据一致性和业务完整性 + /// + [DynamicApi(Area = "app")] + public class TaskAssignmentModificationService : BaseService, IDynamicApi + { + private readonly IIntegrationRecordRepository _integrationRecordRepository; + private readonly WorkOrderRepository _workOrderRepository; + private readonly ILogger _logger; + + public TaskAssignmentModificationService( + IIntegrationRecordRepository integrationRecordRepository, + WorkOrderRepository workOrderRepository, + ILogger logger) + { + _integrationRecordRepository = integrationRecordRepository; + _workOrderRepository = workOrderRepository; + _logger = logger; + } + + /// + /// 修改任务人员分配 + /// 深度业务思考:根据选中的人员精确修改任务分配,同时保持整合记录的一致性 + /// 核心功能: + /// 1. 根据选中的人员修改任务的指定人员ID和姓名 + /// 2. 更新整合记录的修改状态和备注 + /// 3. 记录修改历史和审计信息 + /// + [HttpPost] + public async Task ModifyTaskAssignmentAsync(TaskAssignmentModificationInput input) + { + var result = new TaskAssignmentModificationResult + { + StartTime = DateTime.Now, + IsSuccess = false + }; + + try + { + _logger.LogInformation("开始修改任务人员分配: IntegrationRecordId={IntegrationRecordId}, TaskCount={TaskCount}", + input.IntegrationRecordId, input.TaskAssignmentModifications.Count); + + // 第一步:验证整合记录存在性 + var integrationRecord = await ValidateIntegrationRecordAsync(input.IntegrationRecordId); + if (integrationRecord == null) + { + result.ErrorMessage = $"未找到ID为{input.IntegrationRecordId}的整合记录"; + return result; + } + + result.IntegrationBatchCode = integrationRecord.IntegrationBatchCode; + + // 第二步:获取和验证任务 + var validationResult = await ValidateAndGetTasksAsync(input.TaskAssignmentModifications); + if (!validationResult.IsValid) + { + result.ErrorMessage = validationResult.ErrorMessage; + result.ValidationErrors = validationResult.ValidationErrors; + return result; + } + + var tasks = validationResult.Tasks; + result.TotalTaskCount = tasks.Count; + + // 第三步:执行任务分配修改 + var modificationResults = await ExecuteTaskAssignmentModificationsAsync( + tasks, + input.TaskAssignmentModifications, + input.OperatorUserId, + input.OperatorUserName); + + result.SuccessfulModifications = modificationResults.SuccessfulModifications; + result.FailedModifications = modificationResults.FailedModifications; + result.SuccessfulCount = modificationResults.SuccessfulModifications.Count; + result.FailedCount = modificationResults.FailedModifications.Count; + + // 第四步:更新整合记录 + await UpdateIntegrationRecordAsync(integrationRecord, input, result); + + // 第五步:生成修改摘要 + result.ModificationSummary = GenerateModificationSummary(result); + result.IsSuccess = result.FailedCount == 0; + result.CompletedTime = DateTime.Now; + result.ElapsedMilliseconds = (int)(result.CompletedTime - result.StartTime).TotalMilliseconds; + + _logger.LogInformation("任务人员分配修改完成: Success={IsSuccess}, SuccessCount={SuccessCount}, FailedCount={FailedCount}", + result.IsSuccess, result.SuccessfulCount, result.FailedCount); + + return result; + } + catch (Exception ex) + { + result.IsSuccess = false; + result.ErrorMessage = $"修改任务人员分配时发生异常: {ex.Message}"; + result.CompletedTime = DateTime.Now; + result.ElapsedMilliseconds = (int)(result.CompletedTime - result.StartTime).TotalMilliseconds; + + _logger.LogError(ex, "修改任务人员分配失败: IntegrationRecordId={IntegrationRecordId}, Error={Message}", + input.IntegrationRecordId, ex.Message); + + return result; + } + } + + /// + /// 批量修改任务设备分配 + /// 深度业务思考:支持批量设备分配修改,提高操作效率 + /// + [HttpPost] + public async Task ModifyTaskEquipmentAssignmentAsync(TaskEquipmentAssignmentModificationInput input) + { + var result = new TaskAssignmentModificationResult + { + StartTime = DateTime.Now, + IsSuccess = false + }; + + try + { + _logger.LogInformation("开始修改任务设备分配: IntegrationRecordId={IntegrationRecordId}, TaskCount={TaskCount}", + input.IntegrationRecordId, input.TaskEquipmentModifications.Count); + + // 验证整合记录 + var integrationRecord = await ValidateIntegrationRecordAsync(input.IntegrationRecordId); + if (integrationRecord == null) + { + result.ErrorMessage = $"未找到ID为{input.IntegrationRecordId}的整合记录"; + return result; + } + + result.IntegrationBatchCode = integrationRecord.IntegrationBatchCode; + + // 获取任务并执行设备分配修改 + var taskIds = input.TaskEquipmentModifications.Select(m => m.TaskId).ToList(); + var tasks = await GetTasksByIdsAsync(taskIds); + + result.TotalTaskCount = tasks.Count; + var successfulModifications = new List(); + var failedModifications = new List(); + + foreach (var modification in input.TaskEquipmentModifications) + { + var task = tasks.FirstOrDefault(t => t.Id == modification.TaskId); + if (task == null) + { + failedModifications.Add(new TaskModificationFailure + { + TaskId = modification.TaskId, + TaskCode = modification.TaskCode ?? "未知", + FailureReason = "任务不存在", + ErrorDetails = "未找到指定的任务" + }); + continue; + } + + try + { + // 记录修改前的值 + var previousEquipmentId = task.AssignedEquipmentId; + var previousEquipmentName = task.AssignedEquipmentName; + + // 执行设备分配修改 + task.AssignedEquipmentId = modification.NewEquipmentId; + task.AssignedEquipmentName = modification.NewEquipmentName ?? string.Empty; + + // 添加修改备注 + if (!string.IsNullOrWhiteSpace(modification.ModificationNote)) + { + task.Remarks = string.IsNullOrWhiteSpace(task.Remarks) + ? $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {modification.ModificationNote}" + : $"{task.Remarks}\n[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {modification.ModificationNote}"; + } + + task.ModifiedTime = DateTime.Now; + task.ModifiedUserId = input.OperatorUserId; + task.ModifiedUserName = input.OperatorUserName; + + successfulModifications.Add(new TaskModificationDetail + { + TaskId = task.Id, + TaskCode = task.WorkOrderCode ?? task.Code ?? $"TASK-{task.Id}", + ModificationType = "设备分配修改", + PreviousValue = $"设备ID:{previousEquipmentId}, 设备名:{previousEquipmentName}", + NewValue = $"设备ID:{task.AssignedEquipmentId}, 设备名:{task.AssignedEquipmentName}", + ModificationTime = DateTime.Now, + ModificationNote = modification.ModificationNote ?? "" + }); + } + catch (Exception ex) + { + failedModifications.Add(new TaskModificationFailure + { + TaskId = modification.TaskId, + TaskCode = modification.TaskCode ?? "未知", + FailureReason = "修改异常", + ErrorDetails = ex.Message + }); + } + } + + // 批量更新任务 + if (successfulModifications.Any()) + { + var modifiedTasks = tasks.Where(t => successfulModifications.Any(s => s.TaskId == t.Id)).ToList(); + await _workOrderRepository.UpdateAsync(modifiedTasks); + } + + result.SuccessfulModifications = successfulModifications; + result.FailedModifications = failedModifications; + result.SuccessfulCount = successfulModifications.Count; + result.FailedCount = failedModifications.Count; + + // 更新整合记录 + await UpdateIntegrationRecordForEquipmentModificationAsync(integrationRecord, input, result); + + result.ModificationSummary = $"设备分配修改完成:成功{result.SuccessfulCount}个,失败{result.FailedCount}个"; + result.IsSuccess = result.FailedCount == 0; + result.CompletedTime = DateTime.Now; + result.ElapsedMilliseconds = (int)(result.CompletedTime - result.StartTime).TotalMilliseconds; + + return result; + } + catch (Exception ex) + { + result.IsSuccess = false; + result.ErrorMessage = $"修改任务设备分配时发生异常: {ex.Message}"; + result.CompletedTime = DateTime.Now; + result.ElapsedMilliseconds = (int)(result.CompletedTime - result.StartTime).TotalMilliseconds; + + _logger.LogError(ex, "修改任务设备分配失败: IntegrationRecordId={IntegrationRecordId}, Error={Message}", + input.IntegrationRecordId, ex.Message); + + return result; + } + } + + /// + /// 查询任务分配修改历史 + /// 深度业务思考:提供完整的修改历史追踪,支持审计和分析 + /// + [HttpGet] + public async Task> GetTaskAssignmentModificationHistoryAsync(long integrationRecordId) + { + try + { + // 查询整合记录 + var integrationRecord = await _integrationRecordRepository.GetAsync(integrationRecordId); + if (integrationRecord == null) + { + return new List(); + } + + // 获取相关任务的修改历史 + var taskIds = integrationRecord.GetTaskIds(); + if (!taskIds.Any()) + { + return new List(); + } + + var tasks = await _workOrderRepository.Select + .Where(t => taskIds.Contains(t.Id)) + .Where(t => !string.IsNullOrEmpty(t.Remarks) && t.Remarks.Contains("人员分配修改")) + .OrderByDescending(t => t.ModifiedTime) + .ToListAsync(); + + // 解析修改历史 + var histories = new List(); + foreach (var task in tasks) + { + var history = ParseModificationHistory(task); + if (history != null) + { + histories.Add(history); + } + } + + return histories; + } + catch (Exception ex) + { + _logger.LogError(ex, "查询任务分配修改历史失败: IntegrationRecordId={IntegrationRecordId}, Error={Message}", + integrationRecordId, ex.Message); + return new List(); + } + } + + #region 私有辅助方法 + + /// + /// 验证整合记录存在性 + /// + private async Task ValidateIntegrationRecordAsync(long integrationRecordId) + { + return await _integrationRecordRepository.GetAsync(integrationRecordId); + } + + /// + /// 验证和获取任务 + /// + private async Task ValidateAndGetTasksAsync(List modifications) + { + var result = new TaskValidationResult { IsValid = true }; + + if (!modifications.Any()) + { + result.IsValid = false; + result.ErrorMessage = "没有提供需要修改的任务"; + return result; + } + + var taskIds = modifications.Select(m => m.TaskId).ToList(); + var tasks = await GetTasksByIdsAsync(taskIds); + + if (tasks.Count != taskIds.Count) + { + var foundTaskIds = tasks.Select(t => t.Id).ToList(); + var missingTaskIds = taskIds.Except(foundTaskIds).ToList(); + result.IsValid = false; + result.ErrorMessage = $"未找到以下任务: {string.Join(", ", missingTaskIds)}"; + result.ValidationErrors = missingTaskIds.Select(id => $"任务ID {id} 不存在").ToList(); + return result; + } + + result.Tasks = tasks; + return result; + } + + /// + /// 根据任务ID列表获取任务 + /// + private async Task> GetTasksByIdsAsync(List taskIds) + { + return await _workOrderRepository.Select + .Where(t => taskIds.Contains(t.Id)) + .ToListAsync(); + } + + /// + /// 执行任务分配修改 + /// + private async Task ExecuteTaskAssignmentModificationsAsync( + List tasks, + List modifications, + long operatorUserId, + string operatorUserName) + { + var result = new TaskModificationExecutionResult(); + + foreach (var modification in modifications) + { + var task = tasks.FirstOrDefault(t => t.Id == modification.TaskId); + if (task == null) continue; + + try + { + _logger.LogInformation("修改任务人员分配: TaskId={TaskId}, TaskCode={TaskCode}, NewPersonnelId={PersonnelId}", + modification.TaskId, task.WorkOrderCode ?? task.Code, modification.NewPersonnelId); + + // 记录修改前的值 + var previousPersonnelId = task.AssignedPersonnelId; + var previousPersonnelName = task.AssignedPersonnelName; + + // 执行人员分配修改 + task.AssignedPersonnelId = modification.NewPersonnelId; + task.AssignedPersonnelName = modification.NewPersonnelName ?? string.Empty; + + // 如果同时修改设备分配 + var previousEquipmentId = task.AssignedEquipmentId; + var previousEquipmentName = task.AssignedEquipmentName; + + if (modification.NewEquipmentId.HasValue) + { + task.AssignedEquipmentId = modification.NewEquipmentId; + task.AssignedEquipmentName = modification.NewEquipmentName ?? string.Empty; + } + + // 更新任务备注 + var modificationNote = $"人员分配修改:{previousPersonnelName}({previousPersonnelId}) → {task.AssignedPersonnelName}({task.AssignedPersonnelId})"; + + if (modification.NewEquipmentId.HasValue) + { + modificationNote += $";设备分配修改:{previousEquipmentName}({previousEquipmentId}) → {task.AssignedEquipmentName}({task.AssignedEquipmentId})"; + } + + if (!string.IsNullOrWhiteSpace(modification.ModificationNote)) + { + modificationNote += $";备注:{modification.ModificationNote}"; + } + + task.Remarks = string.IsNullOrWhiteSpace(task.Remarks) + ? $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {modificationNote}" + : $"{task.Remarks}\n[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {modificationNote}"; + + task.ModifiedTime = DateTime.Now; + task.ModifiedUserId = operatorUserId; + task.ModifiedUserName = operatorUserName; + + result.SuccessfulModifications.Add(new TaskModificationDetail + { + TaskId = task.Id, + TaskCode = task.WorkOrderCode ?? task.Code ?? $"TASK-{task.Id}", + ModificationType = "人员分配修改", + PreviousValue = $"人员ID:{previousPersonnelId}, 人员名:{previousPersonnelName}", + NewValue = $"人员ID:{task.AssignedPersonnelId}, 人员名:{task.AssignedPersonnelName}", + ModificationTime = DateTime.Now, + ModificationNote = modification.ModificationNote ?? "" + }); + } + catch (Exception ex) + { + result.FailedModifications.Add(new TaskModificationFailure + { + TaskId = modification.TaskId, + TaskCode = task.WorkOrderCode ?? task.Code ?? $"TASK-{task.Id}", + FailureReason = "修改异常", + ErrorDetails = ex.Message + }); + + _logger.LogError(ex, "修改任务人员分配失败: TaskId={TaskId}, Error={Message}", + modification.TaskId, ex.Message); + } + } + + // 批量更新成功修改的任务 + if (result.SuccessfulModifications.Any()) + { + var modifiedTasks = tasks.Where(t => result.SuccessfulModifications.Any(s => s.TaskId == t.Id)).ToList(); + await _workOrderRepository.UpdateAsync(modifiedTasks); + } + + return result; + } + + /// + /// 更新整合记录 + /// + private async Task UpdateIntegrationRecordAsync( + IntegrationRecordEntity integrationRecord, + TaskAssignmentModificationInput input, + TaskAssignmentModificationResult result) + { + try + { + // 更新整合记录的修改信息 + var modificationSummary = $"任务人员分配修改:成功{result.SuccessfulCount}个,失败{result.FailedCount}个"; + + if (!string.IsNullOrWhiteSpace(input.ModificationReason)) + { + modificationSummary = $"{input.ModificationReason} - {modificationSummary}"; + } + + integrationRecord.Remarks = string.IsNullOrWhiteSpace(integrationRecord.Remarks) + ? $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {modificationSummary}" + : $"{integrationRecord.Remarks}\n[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {modificationSummary}"; + + integrationRecord.ModifiedTime = DateTime.Now; + integrationRecord.ModifiedUserId = input.OperatorUserId; + integrationRecord.ModifiedUserName = input.OperatorUserName; + + // 更新人员分配结果中的successfulMatches + if (result.SuccessfulModifications.Any()) + { + try + { + // 反序列化现有的人员分配结果 + if (!string.IsNullOrEmpty(integrationRecord.PersonnelAllocationResultJson)) + { + var personnelAllocationResult = JsonSerializer.Deserialize( + integrationRecord.PersonnelAllocationResultJson, + new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + + if (personnelAllocationResult?.SuccessfulMatches != null) + { + // 更新successfulMatches中对应的匹配信息 + foreach (var modification in result.SuccessfulModifications) + { + var existingMatch = personnelAllocationResult.SuccessfulMatches + .FirstOrDefault(m => m.TaskId == modification.TaskId); + + if (existingMatch != null) + { + // 更新人员信息 + existingMatch.PersonnelId = modification.NewValue.Contains("人员ID:") + ? long.Parse(modification.NewValue.Split(',')[0].Split(':')[1]) + : existingMatch.PersonnelId; + + existingMatch.PersonnelName = modification.NewValue.Contains("人员名:") + ? modification.NewValue.Split(',')[1].Split(':')[1] + : existingMatch.PersonnelName; + } + } + + // 重新序列化并更新 + integrationRecord.PersonnelAllocationResultJson = JsonSerializer.Serialize( + personnelAllocationResult, + new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true + }); + } + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "更新人员分配结果失败: IntegrationRecordId={IntegrationRecordId}, Error={Message}", + integrationRecord.Id, ex.Message); + } + + // 更新设备分配结果中的successfulMatches(如果同时修改了设备) + try + { + // 反序列化现有的设备分配结果 + if (!string.IsNullOrEmpty(integrationRecord.EquipmentAllocationResultJson)) + { + var equipmentAllocationResult = JsonSerializer.Deserialize( + integrationRecord.EquipmentAllocationResultJson, + new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + + if (equipmentAllocationResult?.SuccessfulMatches != null) + { + // 更新successfulMatches中对应的匹配信息 + foreach (var modification in result.SuccessfulModifications) + { + // 检查修改备注中是否包含设备信息 + if (modification.NewValue.Contains("设备分配修改")) + { + var existingMatch = equipmentAllocationResult.SuccessfulMatches + .FirstOrDefault(m => m.TaskId == modification.TaskId); + + if (existingMatch != null) + { + // 从备注中提取设备信息 + var equipmentInfo = ExtractEquipmentInfoFromModification(modification.NewValue); + if (equipmentInfo.HasValue) + { + existingMatch.EquipmentId = equipmentInfo.Value.equipmentId; + existingMatch.EquipmentName = equipmentInfo.Value.equipmentName; + } + } + } + } + + // 重新序列化并更新 + integrationRecord.EquipmentAllocationResultJson = JsonSerializer.Serialize( + equipmentAllocationResult, + new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true + }); + } + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "更新设备分配结果失败: IntegrationRecordId={IntegrationRecordId}, Error={Message}", + integrationRecord.Id, ex.Message); + } + } + + await _integrationRecordRepository.UpdateAsync(integrationRecord); + } + catch (Exception ex) + { + _logger.LogError(ex, "更新整合记录失败: IntegrationRecordId={IntegrationRecordId}, Error={Message}", + integrationRecord.Id, ex.Message); + } + } + + /// + /// 从修改备注中提取设备信息 + /// + private (long equipmentId, string equipmentName)? ExtractEquipmentInfoFromModification(string modificationValue) + { + try + { + // 解析类似 "设备分配修改:设备ID:1001, 设备名:设备A" 的字符串 + if (modificationValue.Contains("设备ID:") && modificationValue.Contains("设备名:")) + { + var equipmentIdStr = modificationValue.Split(new[] { "设备ID:", "," }, StringSplitOptions.None)[1]; + var equipmentNameStr = modificationValue.Split(new[] { "设备名:" }, StringSplitOptions.None)[1]; + + if (long.TryParse(equipmentIdStr, out long equipmentId)) + { + return (equipmentId, equipmentNameStr); + } + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "解析设备信息失败: ModificationValue={ModificationValue}", modificationValue); + } + + return null; + } + + /// + /// 为设备修改更新整合记录 + /// + private async Task UpdateIntegrationRecordForEquipmentModificationAsync( + IntegrationRecordEntity integrationRecord, + TaskEquipmentAssignmentModificationInput input, + TaskAssignmentModificationResult result) + { + try + { + var modificationSummary = $"任务设备分配修改:成功{result.SuccessfulCount}个,失败{result.FailedCount}个"; + + if (!string.IsNullOrWhiteSpace(input.ModificationReason)) + { + modificationSummary = $"{input.ModificationReason} - {modificationSummary}"; + } + + integrationRecord.Remarks = string.IsNullOrWhiteSpace(integrationRecord.Remarks) + ? $"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {modificationSummary}" + : $"{integrationRecord.Remarks}\n[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {modificationSummary}"; + + integrationRecord.ModifiedTime = DateTime.Now; + integrationRecord.ModifiedUserId = input.OperatorUserId; + integrationRecord.ModifiedUserName = input.OperatorUserName; + + // 更新设备分配结果中的successfulMatches + if (result.SuccessfulModifications.Any()) + { + try + { + // 反序列化现有的设备分配结果 + if (!string.IsNullOrEmpty(integrationRecord.EquipmentAllocationResultJson)) + { + var equipmentAllocationResult = JsonSerializer.Deserialize( + integrationRecord.EquipmentAllocationResultJson, + new JsonSerializerOptions { PropertyNameCaseInsensitive = true }); + + if (equipmentAllocationResult?.SuccessfulMatches != null) + { + // 更新successfulMatches中对应的匹配信息 + foreach (var modification in result.SuccessfulModifications) + { + var existingMatch = equipmentAllocationResult.SuccessfulMatches + .FirstOrDefault(m => m.TaskId == modification.TaskId); + + if (existingMatch != null) + { + // 更新设备信息 + existingMatch.EquipmentId = modification.NewValue.Contains("设备ID:") + ? long.Parse(modification.NewValue.Split(',')[0].Split(':')[1]) + : existingMatch.EquipmentId; + + existingMatch.EquipmentName = modification.NewValue.Contains("设备名:") + ? modification.NewValue.Split(',')[1].Split(':')[1] + : existingMatch.EquipmentName; + } + } + + // 重新序列化并更新 + integrationRecord.EquipmentAllocationResultJson = JsonSerializer.Serialize( + equipmentAllocationResult, + new JsonSerializerOptions + { + PropertyNamingPolicy = JsonNamingPolicy.CamelCase, + WriteIndented = true + }); + } + } + } + catch (Exception ex) + { + _logger.LogWarning(ex, "更新设备分配结果失败: IntegrationRecordId={IntegrationRecordId}, Error={Message}", + integrationRecord.Id, ex.Message); + } + } + + await _integrationRecordRepository.UpdateAsync(integrationRecord); + } + catch (Exception ex) + { + _logger.LogError(ex, "更新整合记录失败: IntegrationRecordId={IntegrationRecordId}, Error={Message}", + integrationRecord.Id, ex.Message); + } + } + + /// + /// 生成修改摘要 + /// + private string GenerateModificationSummary(TaskAssignmentModificationResult result) + { + var summary = $"任务分配修改完成:总计{result.TotalTaskCount}个任务,成功{result.SuccessfulCount}个,失败{result.FailedCount}个"; + + if (result.FailedCount > 0) + { + var failureReasons = result.FailedModifications + .GroupBy(f => f.FailureReason) + .Select(g => $"{g.Key}({g.Count()}个)") + .ToList(); + summary += $"。失败原因:{string.Join("、", failureReasons)}"; + } + + summary += $"。耗时{result.ElapsedMilliseconds}毫秒"; + return summary; + } + + /// + /// 解析修改历史 + /// + private TaskAssignmentModificationHistory ParseModificationHistory(WorkOrderEntity task) + { + try + { + // 简单的历史解析实现 + return new TaskAssignmentModificationHistory + { + TaskId = task.Id, + TaskCode = task.WorkOrderCode ?? task.Code ?? $"TASK-{task.Id}", + ModificationTime = task.ModifiedTime ?? DateTime.Now, + ModificationDetails = task.Remarks ?? "", + ModifiedByUserId = task.ModifiedUserId, + ModifiedByUserName = task.ModifiedUserName ?? "", + CurrentPersonnelId = task.AssignedPersonnelId, + CurrentPersonnelName = task.AssignedPersonnelName ?? "", + CurrentEquipmentId = task.AssignedEquipmentId, + CurrentEquipmentName = task.AssignedEquipmentName ?? "" + }; + } + catch (Exception ex) + { + _logger.LogWarning(ex, "解析任务修改历史失败: TaskId={TaskId}, Error={Message}", task.Id, ex.Message); + return null; + } + } + + #endregion + + #region 内部数据模型 + + /// + /// 任务验证结果 + /// + private class TaskValidationResult + { + public bool IsValid { get; set; } + public string ErrorMessage { get; set; } = ""; + public List ValidationErrors { get; set; } = new(); + public List Tasks { get; set; } = new(); + } + + /// + /// 任务修改执行结果 + /// + private class TaskModificationExecutionResult + { + public List SuccessfulModifications { get; set; } = new(); + public List FailedModifications { get; set; } = new(); + } + + #endregion + } + + #region 输入输出模型 + + /// + /// 任务人员分配修改输入 + /// + public class TaskAssignmentModificationInput + { + /// + /// 整合记录ID + /// + public long IntegrationRecordId { get; set; } + + /// + /// 操作员用户ID + /// + public long OperatorUserId { get; set; } + + /// + /// 操作员用户名 + /// + public string OperatorUserName { get; set; } = string.Empty; + + /// + /// 任务分配修改列表 + /// + public List TaskAssignmentModifications { get; set; } = new(); + + /// + /// 修改原因说明 + /// + public string ModificationReason { get; set; } = string.Empty; + + /// + /// 是否强制执行(忽略部分警告) + /// + public bool ForceExecute { get; set; } = false; + } + + /// + /// 任务设备分配修改输入 + /// + public class TaskEquipmentAssignmentModificationInput + { + /// + /// 整合记录ID + /// + public long IntegrationRecordId { get; set; } + + /// + /// 操作员用户ID + /// + public long OperatorUserId { get; set; } + + /// + /// 操作员用户名 + /// + public string OperatorUserName { get; set; } = string.Empty; + + /// + /// 任务设备分配修改列表 + /// + public List TaskEquipmentModifications { get; set; } = new(); + + /// + /// 修改原因说明 + /// + public string ModificationReason { get; set; } = string.Empty; + + /// + /// 是否强制执行(忽略部分警告) + /// + public bool ForceExecute { get; set; } = false; + } + + /// + /// 单个任务人员分配修改 + /// + public class TaskAssignmentModification + { + /// + /// 任务ID + /// + public long TaskId { get; set; } + + /// + /// 任务代码(可选,用于显示) + /// + public string? TaskCode { get; set; } + + /// + /// 新的人员ID + /// + public long? NewPersonnelId { get; set; } + + /// + /// 新的人员姓名 + /// + public string? NewPersonnelName { get; set; } + + /// + /// 新的设备ID(可选,支持同时修改设备) + /// + public long? NewEquipmentId { get; set; } + + /// + /// 新的设备名称(可选) + /// + public string? NewEquipmentName { get; set; } + + /// + /// 修改备注 + /// + public string ModificationNote { get; set; } = string.Empty; + } + + /// + /// 单个任务设备分配修改 + /// + public class TaskEquipmentAssignmentModification + { + /// + /// 任务ID + /// + public long TaskId { get; set; } + + /// + /// 任务代码(可选,用于显示) + /// + public string? TaskCode { get; set; } + + /// + /// 新的设备ID + /// + public long? NewEquipmentId { get; set; } + + /// + /// 新的设备名称 + /// + public string? NewEquipmentName { get; set; } + + /// + /// 修改备注 + /// + public string ModificationNote { get; set; } = string.Empty; + } + + /// + /// 任务分配修改结果 + /// + public class TaskAssignmentModificationResult + { + /// + /// 是否修改成功 + /// + public bool IsSuccess { get; set; } + + /// + /// 开始时间 + /// + public DateTime StartTime { get; set; } + + /// + /// 完成时间 + /// + public DateTime CompletedTime { get; set; } + + /// + /// 耗时毫秒数 + /// + public int ElapsedMilliseconds { get; set; } + + /// + /// 整合批次编码 + /// + public string IntegrationBatchCode { get; set; } = string.Empty; + + /// + /// 总任务数量 + /// + public int TotalTaskCount { get; set; } + + /// + /// 成功修改数量 + /// + public int SuccessfulCount { get; set; } + + /// + /// 失败修改数量 + /// + public int FailedCount { get; set; } + + /// + /// 成功修改的任务详情 + /// + public List SuccessfulModifications { get; set; } = new(); + + /// + /// 失败修改的任务详情 + /// + public List FailedModifications { get; set; } = new(); + + /// + /// 修改摘要 + /// + public string ModificationSummary { get; set; } = string.Empty; + + /// + /// 错误消息 + /// + public string ErrorMessage { get; set; } = string.Empty; + + /// + /// 验证错误列表 + /// + public List ValidationErrors { get; set; } = new(); + } + + /// + /// 任务修改详情 + /// + public class TaskModificationDetail + { + /// + /// 任务ID + /// + public long TaskId { get; set; } + + /// + /// 任务代码 + /// + public string TaskCode { get; set; } = string.Empty; + + /// + /// 修改类型 + /// + public string ModificationType { get; set; } = string.Empty; + + /// + /// 修改前的值 + /// + public string PreviousValue { get; set; } = string.Empty; + + /// + /// 修改后的值 + /// + public string NewValue { get; set; } = string.Empty; + + /// + /// 修改时间 + /// + public DateTime ModificationTime { get; set; } + + /// + /// 修改备注 + /// + public string ModificationNote { get; set; } = string.Empty; + } + + /// + /// 任务修改失败详情 + /// + public class TaskModificationFailure + { + /// + /// 任务ID + /// + public long TaskId { get; set; } + + /// + /// 任务代码 + /// + public string TaskCode { get; set; } = string.Empty; + + /// + /// 失败原因 + /// + public string FailureReason { get; set; } = string.Empty; + + /// + /// 错误详情 + /// + public string ErrorDetails { get; set; } = string.Empty; + } + + /// + /// 任务分配修改历史 + /// + public class TaskAssignmentModificationHistory + { + /// + /// 任务ID + /// + public long TaskId { get; set; } + + /// + /// 任务代码 + /// + public string TaskCode { get; set; } = string.Empty; + + /// + /// 修改时间 + /// + public DateTime ModificationTime { get; set; } + + /// + /// 修改详情 + /// + public string ModificationDetails { get; set; } = string.Empty; + + /// + /// 修改人用户ID + /// + public long? ModifiedByUserId { get; set; } + + /// + /// 修改人用户名 + /// + public string ModifiedByUserName { get; set; } = string.Empty; + + /// + /// 当前人员ID + /// + public long? CurrentPersonnelId { get; set; } + + /// + /// 当前人员姓名 + /// + public string CurrentPersonnelName { get; set; } = string.Empty; + + /// + /// 当前设备ID + /// + public long? CurrentEquipmentId { get; set; } + + /// + /// 当前设备名称 + /// + public string CurrentEquipmentName { get; set; } = string.Empty; + } + + #endregion +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api/Services/Integration/WorkOrderIntegrationOperationService.cs b/NPP.SmartSchedue.Api/Services/Integration/WorkOrderIntegrationOperationService.cs new file mode 100644 index 0000000..3402dcb --- /dev/null +++ b/NPP.SmartSchedue.Api/Services/Integration/WorkOrderIntegrationOperationService.cs @@ -0,0 +1,987 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text.Json; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using NPP.SmartSchedue.Api.Contracts.Core.Enums; +using NPP.SmartSchedue.Api.Contracts.Domain.Integration; +using NPP.SmartSchedue.Api.Contracts.Domain.Work; +using NPP.SmartSchedue.Api.Contracts.Services.Integration; +using NPP.SmartSchedue.Api.Contracts.Services.Integration.Input; +using NPP.SmartSchedue.Api.Contracts.Services.Integration.Models; +using NPP.SmartSchedue.Api.Contracts.Services.Integration.Output; +using NPP.SmartSchedue.Api.Contracts.Services.Work; +using NPP.SmartSchedue.Api.Services.Notification; +using NPP.SmartSchedue.Api.Contracts.Services.Work.Input; +using NPP.SmartSchedue.Api.Repositories.Work; +using NPP.SmartSchedue.Api.Repositories.Integration; +using ZhonTai.Admin.Services; +using ZhonTai.DynamicApi; +using ZhonTai.DynamicApi.Attributes; + +namespace NPP.SmartSchedue.Api.Services.Integration +{ + /// + /// 工作订单整合操作统一服务实现 + /// + [DynamicApi(Area = "app")] + public class WorkOrderIntegrationOperationService : BaseService, IWorkOrderIntegrationOperationService, IDynamicApi + { + private readonly IWorkOrderService _workOrderService; + private readonly IIntegrationRecordService _integrationRecordService; + private readonly WorkOrderRepository _workOrderRepository; + private readonly IntegrationRecordRepository _integrationRecordRepository; + private readonly WorkOrderStatusValidator _statusValidator; + private readonly ILogger _logger; + private readonly NotificationScheduledService _notificationScheduledService; + + public WorkOrderIntegrationOperationService( + IWorkOrderService workOrderService, + IIntegrationRecordService integrationRecordService, + WorkOrderRepository workOrderRepository, + IntegrationRecordRepository integrationRecordRepository, + WorkOrderStatusValidator statusValidator, + ILogger logger, + NotificationScheduledService notificationScheduledService) + { + _workOrderService = workOrderService; + _integrationRecordService = integrationRecordService; + _workOrderRepository = workOrderRepository; + _integrationRecordRepository = integrationRecordRepository; + _statusValidator = statusValidator; + _logger = logger; + _notificationScheduledService = notificationScheduledService; + } + + /// + /// 修改任务统一入口 + /// + [HttpPost] + public async Task ModifyWorkOrderAsync(WorkOrderModifyOperationInput input) + { + try + { + // 1. 参数验证 + var paramValidation = ValidateModifyParameters(input); + if (!paramValidation.IsValid) + { + return CreateFailResult(paramValidation.ErrorMessage); + } + + // 2. 一次性查询任务,获取所有需要的信息 + var task = await _workOrderRepository.GetAsync(input.TaskId); + if (task == null) + { + return CreateFailResult("任务不存在"); + } + + // 3. 基于已查询的任务进行状态验证 + var statusValidation = _statusValidator.ValidateModifyStatus(task); + if (!statusValidation.IsValid) + { + return CreateFailResult($"状态验证失败:{statusValidation.ErrorMessage}"); + } + + // 4. 业务规则检查 + var businessValidation = await ValidateModifyBusinessRulesAsync(input); + if (!businessValidation.IsValid) + { + return CreateFailResult($"业务规则验证失败:{businessValidation.ErrorMessage}"); + } + + // 5. 保存原始任务信息,用于变更分析 + var originalTask = new WorkOrderEntity + { + Id = task.Id, + WorkOrderDate = task.WorkOrderDate, + AssignedPersonnelId = task.AssignedPersonnelId, + AssignedEquipmentId = task.AssignedEquipmentId, + ProcessId = task.ProcessId, + ShiftId = task.ShiftId, + Priority = task.Priority, + Urgency = task.Urgency, + EstimatedHours = task.EstimatedHours, + Status = task.Status + }; + + // 6. 判断是否需要重新分配 + bool needsReallocation = ShouldTriggerReallocation(originalTask, input.TaskModifyData); + + if (needsReallocation) + { + _logger.LogInformation("任务{TaskId}修改触发重新分配条件", input.TaskId); + } + + // 8. 更新整合记录(仅对已分配状态的任务) + long? affectedIntegrationRecordId = null; + bool reallocationTriggered = false; + + if (originalTask.Status == (int)WorkOrderStatusEnum.Assigned && task.IntegrationRecordId.HasValue) + { + var integrationRecord = await _integrationRecordRepository.GetAsync(task.IntegrationRecordId.Value); + if (integrationRecord != null) + { + await UpdateIntegrationRecordAfterModifyAsync(integrationRecord, input, originalTask, needsReallocation); + affectedIntegrationRecordId = integrationRecord.Id; + } + } + + Mapper.Map(input, task); + task.Status = (int)WorkOrderStatusEnum.PendingSubmit; + task.Remarks = "任务修改需重新智能整合"; + + await _workOrderRepository.UpdateAsync(task); + + // 9. 发送通知 + try + { + // 合并指定人员与任务FL人员 + var designatedIds_Modify = new List(); + if (task.AssignedPersonnelId != null && task.AssignedPersonnelId.HasValue) designatedIds_Modify.AddRange(task.AssignedPersonnelId.Value); + if (task != null && task.WorkOrderFLPersonnels != null && task.WorkOrderFLPersonnels.Any()) designatedIds_Modify.AddRange(task.WorkOrderFLPersonnels.Select(x => x.FLPersonnelId)); + designatedIds_Modify = designatedIds_Modify.Distinct().ToList(); + + await _notificationScheduledService.SendWorkOrderOperationNotificationAsync( + "Modify", input.TaskId, User.Id, User.Name, input.Remarks, designatedIds_Modify); + } + catch (Exception notifyEx) + { + _logger.LogWarning(notifyEx, "修改任务后发送通知失败,TaskId: {TaskId}", input.TaskId); + } + + + // 10. 返回结果 + var resultMessage = needsReallocation ? "任务修改成功,已触发重新分配" : "任务修改成功"; + + return new WorkOrderOperationResult + { + IsSuccess = true, + Message = resultMessage, + AffectedIntegrationRecordId = affectedIntegrationRecordId, + Data = new { + TaskId = input.TaskId, + OperationType = "Modify", + NeedsReallocation = needsReallocation, + ReallocationTriggered = reallocationTriggered + } + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "修改任务时发生错误,TaskId: {TaskId}", input.TaskId); + return CreateFailResult($"修改任务失败:{ex.Message}"); + } + } + + /// + /// 删除任务统一入口 + /// + [HttpPost] + public async Task DeleteWorkOrderAsync(WorkOrderDeleteOperationInput input) + { + try + { + // 1. 参数验证 + var paramValidation = ValidateDeleteParameters(input); + if (!paramValidation.IsValid) + { + return CreateFailResult(paramValidation.ErrorMessage); + } + + // 2. 一次性查询任务,获取所有需要的信息 + var task = await _workOrderRepository.GetAsync(input.TaskId); + if (task == null) + { + return CreateFailResult("任务不存在"); + } + + // 3. 基于已查询的任务进行状态验证 + var statusValidation = _statusValidator.ValidateDeleteStatus(task); + if (!statusValidation.IsValid) + { + return CreateFailResult($"状态验证失败:{statusValidation.ErrorMessage}"); + } + + // 4. 业务规则检查 + var businessValidation = await ValidateDeleteBusinessRulesAsync(input); + if (!businessValidation.IsValid) + { + return CreateFailResult($"业务规则验证失败:{businessValidation.ErrorMessage}"); + } + + // 5. 获取整合记录信息(仅对已分配任务且有关联记录的情况) + IntegrationRecordEntity integrationRecord = null; + if (task.Status == (int)WorkOrderStatusEnum.Assigned && task.IntegrationRecordId.HasValue) + { + integrationRecord = await _integrationRecordRepository.GetAsync(task.IntegrationRecordId.Value); + } + + // 6. 执行任务操作 + await _workOrderService.SoftDeleteAsync(input.TaskId); + + // 7. 更新整合记录(仅对已分配状态的任务) + if (integrationRecord != null) + { + await UpdateIntegrationRecordAfterDeleteAsync(integrationRecord, input); + } + + // 8. 发送通知 + try + { + // 合并指定人员与任务FL人员 + // 合并指定人员与任务FL人员 + var designatedIds_Delete = new List(); + if (task.AssignedPersonnelId != null && task.AssignedPersonnelId.HasValue) designatedIds_Delete.AddRange(task.AssignedPersonnelId.Value); + if (task != null && task.WorkOrderFLPersonnels != null && task.WorkOrderFLPersonnels.Any()) designatedIds_Delete.AddRange(task.WorkOrderFLPersonnels.Select(x => x.FLPersonnelId)); + designatedIds_Delete = designatedIds_Delete.Distinct().ToList(); + + await _notificationScheduledService.SendWorkOrderOperationNotificationAsync( + "Delete", input.TaskId, User.Id, User.Name, input.DeleteReason, designatedIds_Delete); + } + catch (Exception notifyEx) + { + _logger.LogWarning(notifyEx, "删除任务后发送通知失败,TaskId: {TaskId}", input.TaskId); + } + + // 9. 返回结果 + return new WorkOrderOperationResult + { + IsSuccess = true, + Message = input.IsSoftDelete ? "任务软删除成功" : "任务删除成功", + AffectedIntegrationRecordId = integrationRecord?.Id, + Data = new { TaskId = input.TaskId, OperationType = "Delete", IsSoftDelete = input.IsSoftDelete } + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "删除任务时发生错误,TaskId: {TaskId}", input.TaskId); + return CreateFailResult($"删除任务失败:{ex.Message}"); + } + } + + /// + /// 取消任务统一入口 + /// + [HttpPost] + public async Task CancelWorkOrderAsync(WorkOrderCancelOperationInput input) + { + try + { + // 1. 参数验证 + var paramValidation = ValidateCancelParameters(input); + if (!paramValidation.IsValid) + { + return CreateFailResult(paramValidation.ErrorMessage); + } + + // 2. 一次性查询任务,获取所有需要的信息 + var task = await _workOrderRepository.GetAsync(input.TaskId); + if (task == null) + { + return CreateFailResult("任务不存在"); + } + + // 3. 基于已查询的任务进行状态验证 + var statusValidation = _statusValidator.ValidateCancelStatus(task); + if (!statusValidation.IsValid) + { + return CreateFailResult($"状态验证失败:{statusValidation.ErrorMessage}"); + } + + // 4. 业务规则检查 + var businessValidation = await ValidateCancelBusinessRulesAsync(input); + if (!businessValidation.IsValid) + { + return CreateFailResult($"业务规则验证失败:{businessValidation.ErrorMessage}"); + } + + // 5. 获取整合记录信息(仅对已分配任务且有关联记录的情况) + IntegrationRecordEntity integrationRecord = null; + if (task.Status == (int)WorkOrderStatusEnum.Assigned && task.IntegrationRecordId.HasValue) + { + integrationRecord = await _integrationRecordRepository.GetAsync(task.IntegrationRecordId.Value); + } + + // 6. 执行任务操作 - 更新状态为取消 + task.Status = (int)WorkOrderStatusEnum.Cancelled; + task.Remarks = "任务已取消"; + await _workOrderRepository.UpdateAsync(task); + + // 7. 更新整合记录(仅对已分配状态的任务) + if (integrationRecord != null) + { + await UpdateIntegrationRecordAfterCancelAsync(integrationRecord, input); + } + + // 8. 发送通知 + try + { + // 合并指定人员与任务FL人员 + var designatedIds_Cancel = new List(); + if (task.AssignedPersonnelId != null && task.AssignedPersonnelId.HasValue) designatedIds_Cancel.AddRange(task.AssignedPersonnelId.Value); + if (task != null && task.WorkOrderFLPersonnels != null && task.WorkOrderFLPersonnels.Any()) designatedIds_Cancel.AddRange(task.WorkOrderFLPersonnels.Select(x => x.FLPersonnelId)); + designatedIds_Cancel = designatedIds_Cancel.Distinct().ToList(); + + await _notificationScheduledService.SendWorkOrderOperationNotificationAsync( + "Cancel", input.TaskId, User.Id, User.Name, input.CancelReason, designatedIds_Cancel); + } + catch (Exception notifyEx) + { + _logger.LogWarning(notifyEx, "取消任务后发送通知失败,TaskId: {TaskId}", input.TaskId); + } + + // 9. 返回结果 + return new WorkOrderOperationResult + { + IsSuccess = true, + Message = "任务取消成功", + AffectedIntegrationRecordId = integrationRecord?.Id, + Data = new { TaskId = input.TaskId, OperationType = "Cancel" } + }; + } + catch (Exception ex) + { + _logger.LogError(ex, "取消任务时发生错误,TaskId: {TaskId}", input.TaskId); + return CreateFailResult($"取消任务失败:{ex.Message}"); + } + } + + + #region 私有辅助方法 + + /// + /// 判断任务修改是否需要触发重新分配 + /// 基于业务关键字段分析,确保分配策略的准确性 + /// + private bool ShouldTriggerReallocation(WorkOrderEntity originalTask, WorkOrderUpdateInput modifyData) + { + // 1. 时间相关变更(高优先级) + // WorkOrderDate 变更直接影响班次安排和资源可用性 + if (originalTask.WorkOrderDate.Date != modifyData.WorkOrderDate.Date) + { + _logger.LogInformation("任务{TaskId}日期从{OriginalDate}变更为{NewDate},触发重新分配", + originalTask.Id, originalTask.WorkOrderDate.ToString("yyyy-MM-dd"), + modifyData.WorkOrderDate.ToString("yyyy-MM-dd")); + return true; + } + + // 2. 人员设备约束变更(直接影响资源分配) + if (originalTask.AssignedPersonnelId != modifyData.AssignedPersonnelId || + originalTask.AssignedEquipmentId != modifyData.AssignedEquipmentId) + { + _logger.LogInformation("任务{TaskId}资源分配变更,人员:{OriginalPersonnel}->{NewPersonnel},设备:{OriginalEquipment}->{NewEquipment},触发重新分配", + originalTask.Id, + originalTask.AssignedPersonnelId, modifyData.AssignedPersonnelId, + originalTask.AssignedEquipmentId, modifyData.AssignedEquipmentId); + return true; + } + + // 3. 工序或班次变更(影响分配策略和资源池) + if (originalTask.ProcessId != modifyData.ProcessId || + originalTask.ShiftId != modifyData.ShiftId) + { + _logger.LogInformation("任务{TaskId}工序或班次变更,工序:{OriginalProcess}->{NewProcess},班次:{OriginalShift}->{NewShift},触发重新分配", + originalTask.Id, + originalTask.ProcessId, modifyData.ProcessId, + originalTask.ShiftId, modifyData.ShiftId); + return true; + } + + return false; + } + + + /// + /// 修改任务后更新整合记录(仅对已分配任务生效) + /// + private async Task UpdateIntegrationRecordAfterModifyAsync(IntegrationRecordEntity record, WorkOrderModifyOperationInput input, WorkOrderEntity originalTask, bool needsReallocation) + { + if (record == null) return; + + try + { + var modifyData = input.TaskModifyData; + + // === 1. 版本管理字段(每次修改都更新) === + record.CurrentVersion += 1; + record.ChangeTriggerSource = "TaskModification"; + + // === 2. 根据变更类型更新对应字段 === + var changeTypes = new List(); + + // 2.1 日期变更处理 + if (originalTask.WorkOrderDate.Date != modifyData.WorkOrderDate.Date) + { + changeTypes.Add("WorkOrderDateChanged"); + // 日期变更影响所有基于时间的分配,需要标记为需要重新分配 + await UpdateAllocationResultsForDateChange(record, input.TaskId, originalTask, modifyData); + } + + // 2.2 人员分配变更处理 + if (originalTask.AssignedPersonnelId != modifyData.AssignedPersonnelId) + { + changeTypes.Add("PersonnelAssignmentChanged"); + await UpdatePersonnelAllocationResultAfterTaskCancel(record, input.TaskId, "Task AssignmentChanged"); + await RecalculatePersonnelFairnessScore(record); + } + + // 2.3 设备分配变更处理 + if (originalTask.AssignedEquipmentId != modifyData.AssignedEquipmentId) + { + changeTypes.Add("EquipmentAssignmentChanged"); + await UpdateEquipmentAllocationResultAfterTaskCancel(record, input.TaskId, "Task AssignmentChanged"); + await RecalculateEquipmentUtilizationRate(record); + } + + // 2.4 工序/班次变更处理 + if (originalTask.ProcessId != modifyData.ProcessId || originalTask.ShiftId != modifyData.ShiftId) + { + changeTypes.Add("ProcessOrShiftChanged"); + // 工序班次变更可能需要更新分配策略配置 + await UpdateStrategyConfigForProcessShiftChange(record, originalTask, modifyData); + // 重新评估所有分配结果 + await ReEvaluateAllAllocationResults(record, input.TaskId); + } + + // === 3. 统计信息更新 === + if (needsReallocation) + { + // 如果需要重新分配,可能会影响成功失败统计 + // 这里可以标记状态,等待重新分配完成后再更新统计 + record.AllowAutoReallocation = true; + record.ChangeImpactScore = CalculateChangeImpact(changeTypes, originalTask, modifyData); + } + + // === 4. 变更详情记录 === + var changeDetails = record.GetChangeDetails(); + if (!changeDetails.ChangedTaskIds.Contains(input.TaskId)) + { + changeDetails.ChangedTaskIds.Add(input.TaskId); + } + changeDetails.ChangeTypes.AddRange(changeTypes); + changeDetails.DetectedTime = DateTime.Now; + + // 记录具体变更内容到TaskModifications + if (originalTask.WorkOrderDate.Date != modifyData.WorkOrderDate.Date) + { + changeDetails.TaskModifications.Add(new TaskChangeDetail + { + TaskId = input.TaskId, + ModificationType = "TimeChanged", + BeforeValue = System.Text.Json.JsonSerializer.Serialize(new { WorkOrderDate = originalTask.WorkOrderDate }), + AfterValue = System.Text.Json.JsonSerializer.Serialize(new { WorkOrderDate = modifyData.WorkOrderDate }), + Description = "任务日期变更" + }); + } + + if (originalTask.AssignedPersonnelId != modifyData.AssignedPersonnelId) + { + changeDetails.PersonnelChanges.Add(new PersonnelAssignmentChange + { + TaskId = input.TaskId, + PreviousPersonnelId = originalTask.AssignedPersonnelId, + NewPersonnelId = modifyData.AssignedPersonnelId, + ChangeReason = "任务修改导致人员分配变更" + }); + } + + if (originalTask.AssignedEquipmentId != modifyData.AssignedEquipmentId) + { + changeDetails.EquipmentChanges.Add(new EquipmentAssignmentChange + { + TaskId = input.TaskId, + PreviousEquipmentId = originalTask.AssignedEquipmentId, + NewEquipmentId = modifyData.AssignedEquipmentId, + ChangeReason = "任务修改导致设备分配变更" + }); + } + + if (originalTask.ProcessId != modifyData.ProcessId) + { + changeDetails.TaskModifications.Add(new TaskChangeDetail + { + TaskId = input.TaskId, + ModificationType = "ProcessChanged", + BeforeValue = System.Text.Json.JsonSerializer.Serialize(new { ProcessId = originalTask.ProcessId }), + AfterValue = System.Text.Json.JsonSerializer.Serialize(new { ProcessId = modifyData.ProcessId }), + Description = "工序变更" + }); + } + + if (originalTask.ShiftId != modifyData.ShiftId) + { + changeDetails.TaskModifications.Add(new TaskChangeDetail + { + TaskId = input.TaskId, + ModificationType = "ShiftChanged", + BeforeValue = System.Text.Json.JsonSerializer.Serialize(new { ShiftId = originalTask.ShiftId }), + AfterValue = System.Text.Json.JsonSerializer.Serialize(new { ShiftId = modifyData.ShiftId }), + Description = "班次变更" + }); + } + record.SetChangeDetails(changeDetails); + + // === 5. 更新结果摘要 === + var reallocationNote = needsReallocation ? " (需要重新分配)" : ""; + record.ResultSummary = $"总任务数: {record.TotalTaskCount}, 成功: {record.SuccessTaskCount}, 失败: {record.FailedTaskCount}, 成功率: {record.SuccessRate}%{reallocationNote}"; + + // === 6. 操作日志 === + var changeTypeStr = string.Join(", ", changeTypes); + record.Remarks = $"{record.Remarks}\n[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 任务{input.TaskId}修改({changeTypeStr}){reallocationNote}: {input.Remarks}"; + + await _integrationRecordRepository.UpdateAsync(record); + + _logger.LogInformation("任务{TaskId}修改后,整合记录{IntegrationRecordId}已更新,变更类型:{ChangeTypes},需要重新分配:{NeedsReallocation}", + input.TaskId, record.Id, changeTypeStr, needsReallocation); + } + catch (Exception ex) + { + _logger.LogError(ex, "更新整合记录时发生错误,TaskId: {TaskId}, IntegrationRecordId: {IntegrationRecordId}", + input.TaskId, record.Id); + throw; + } + } + + /// + /// 删除任务后更新整合记录(仅对已分配任务生效) + /// + private async Task UpdateIntegrationRecordAfterDeleteAsync(IntegrationRecordEntity record, WorkOrderDeleteOperationInput input) + { + if (record == null) return; + + try + { + // 1. 从 TaskIdsJson 中移除被删除的任务ID + var taskIds = record.GetTaskIds(); + if (taskIds.Contains(input.TaskId)) + { + taskIds.Remove(input.TaskId); + record.SetTaskIds(taskIds); + } + + // 2. 更新任务计数统计(删除的任务直接减少成功数,不增加失败数) + record.SuccessTaskCount = Math.Max(0, record.SuccessTaskCount - 1); + + // 3. 更新人员分配结果(从JSON中移除该任务的分配信息) + await UpdatePersonnelAllocationResultAfterTaskRemoval(record, input.TaskId); + + // 4. 更新设备分配结果(从JSON中移除该任务的分配信息) + await UpdateEquipmentAllocationResultAfterTaskRemoval(record, input.TaskId); + + // 5. 更新结果摘要 + record.ResultSummary = $"总任务数: {record.TotalTaskCount}, 成功: {record.SuccessTaskCount}, 失败: {record.FailedTaskCount}, 成功率: {record.SuccessRate}%"; + + // 6. 记录操作日志 + var deleteType = input.IsSoftDelete ? "软删除" : "物理删除"; + record.Remarks = $"{record.Remarks}\n[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 任务{input.TaskId}被{input.OperatorName}{deleteType}: {input.DeleteReason}"; + + await _integrationRecordRepository.UpdateAsync(record); + + _logger.LogInformation("已分配任务{TaskId}{DeleteType}后,整合记录{IntegrationRecordId}已更新(包含TaskIds、分配结果等)", + input.TaskId, deleteType, record.Id); + } + catch (Exception ex) + { + _logger.LogError(ex, "删除任务后更新整合记录时发生错误,TaskId: {TaskId}, IntegrationRecordId: {IntegrationRecordId}", + input.TaskId, record.Id); + } + } + + /// + /// 取消任务后更新整合记录(仅对已分配任务生效) + /// + private async Task UpdateIntegrationRecordAfterCancelAsync(IntegrationRecordEntity record, WorkOrderCancelOperationInput input) + { + if (record == null) return; + + try + { + // 1. 任务取消后不从TaskIdsJson中移除(取消的任务仍然是原始整合的一部分) + // 2. 更新任务计数统计 + record.SuccessTaskCount = Math.Max(0, record.SuccessTaskCount - 1); + + // 3. 更新人员分配结果(将该任务标记为取消状态) + await UpdatePersonnelAllocationResultAfterTaskCancel(record, input.TaskId, "Task Cancelled"); + + // 4. 更新设备分配结果(将该任务标记为取消状态) + await UpdateEquipmentAllocationResultAfterTaskCancel(record, input.TaskId, "Task Cancelled"); + + // 5. 更新结果摘要 + record.ResultSummary = $"总任务数: {record.TotalTaskCount}, 成功: {record.SuccessTaskCount}, 失败: {record.FailedTaskCount}, 成功率: {record.SuccessRate}%"; + + // 6. 记录操作日志 + record.Remarks = $"{record.Remarks}\n[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] 任务{input.TaskId}被{input.OperatorName}取消: {input.CancelReason}"; + + await _integrationRecordRepository.UpdateAsync(record); + + _logger.LogInformation("已分配任务{TaskId}取消后,整合记录{IntegrationRecordId}已更新(包含分配结果状态等)", input.TaskId, record.Id); + } + catch (Exception ex) + { + _logger.LogError(ex, "取消任务后更新整合记录时发生错误,TaskId: {TaskId}, IntegrationRecordId: {IntegrationRecordId}", + input.TaskId, record.Id); + } + } + + + /// + /// 创建失败结果 + /// + private WorkOrderOperationResult CreateFailResult(string message) + { + return new WorkOrderOperationResult + { + IsSuccess = false, + Message = message, + ErrorDetails = { message } + }; + } + + /// + /// 验证修改操作参数 + /// + private WorkOrderValidationResult ValidateModifyParameters(WorkOrderModifyOperationInput input) + { + if (input == null) + return WorkOrderValidationResult.Fail("输入参数不能为空"); + + if (input.TaskId <= 0) + return WorkOrderValidationResult.Fail("任务ID必须大于0"); + + if (input.TaskModifyData == null) + return WorkOrderValidationResult.Fail("修改数据不能为空"); + + return WorkOrderValidationResult.Success(); + } + + /// + /// 验证删除操作参数 + /// + private WorkOrderValidationResult ValidateDeleteParameters(WorkOrderDeleteOperationInput input) + { + if (input == null) + return WorkOrderValidationResult.Fail("输入参数不能为空"); + + if (input.TaskId <= 0) + return WorkOrderValidationResult.Fail("任务ID必须大于0"); + + return WorkOrderValidationResult.Success(); + } + + /// + /// 验证取消操作参数 + /// + private WorkOrderValidationResult ValidateCancelParameters(WorkOrderCancelOperationInput input) + { + if (input == null) + return WorkOrderValidationResult.Fail("输入参数不能为空"); + + if (input.TaskId <= 0) + return WorkOrderValidationResult.Fail("任务ID必须大于0"); + + return WorkOrderValidationResult.Success(); + } + + /// + /// 验证修改操作的业务规则 + /// + private Task ValidateModifyBusinessRulesAsync(WorkOrderModifyOperationInput input) + { + // 这里可以添加具体的业务规则验证 + // 例如:检查任务是否与其他任务有依赖关系等 + return Task.FromResult(WorkOrderValidationResult.Success()); + } + + /// + /// 验证删除操作的业务规则 + /// + private Task ValidateDeleteBusinessRulesAsync(WorkOrderDeleteOperationInput input) + { + // 这里可以添加具体的业务规则验证 + // 例如:检查任务是否与其他任务有依赖关系等 + return Task.FromResult(WorkOrderValidationResult.Success()); + } + + /// + /// 验证取消操作的业务规则 + /// + private Task ValidateCancelBusinessRulesAsync(WorkOrderCancelOperationInput input) + { + // 这里可以添加具体的业务规则验证 + // 例如:检查任务是否与其他任务有依赖关系等 + return Task.FromResult(WorkOrderValidationResult.Success()); + } + + /// + /// 任务删除后更新人员分配结果 + /// + private async Task UpdatePersonnelAllocationResultAfterTaskRemoval(IntegrationRecordEntity record, long taskId) + { + try + { + if (string.IsNullOrWhiteSpace(record.PersonnelAllocationResultJson) || record.PersonnelAllocationResultJson == "{}") + return; + + // 解析人员分配结果 + var allocationResult = System.Text.Json.JsonSerializer.Deserialize>(record.PersonnelAllocationResultJson); + if (allocationResult == null) return; + + // 从成功分配列表中移除该任务 + if (allocationResult.ContainsKey("successfulAllocations")) + { + var successfulAllocations = allocationResult["successfulAllocations"] as JsonElement?; + if (successfulAllocations.HasValue && successfulAllocations.Value.ValueKind == JsonValueKind.Array) + { + var updatedAllocations = new List(); + foreach (var allocation in successfulAllocations.Value.EnumerateArray()) + { + if (allocation.TryGetProperty("TaskId", out var taskIdElement) && + taskIdElement.GetInt64() != taskId) + { + updatedAllocations.Add(System.Text.Json.JsonSerializer.Deserialize(allocation.GetRawText())); + } + } + allocationResult["successfulAllocations"] = updatedAllocations; + } + } + + // 添加到失败列表 + if (!allocationResult.ContainsKey("failedAllocations")) + allocationResult["failedAllocations"] = new List(); + + var failedList = allocationResult["failedAllocations"] as List ?? new List(); + failedList.Add(new { TaskId = taskId, Reason = "Task deleted", Timestamp = DateTime.Now }); + allocationResult["failedAllocations"] = failedList; + + // 更新JSON + record.PersonnelAllocationResultJson = System.Text.Json.JsonSerializer.Serialize(allocationResult); + } + catch (Exception ex) + { + _logger.LogError(ex, "更新人员分配结果时发生错误,TaskId: {TaskId}", taskId); + } + } + + /// + /// 任务删除后更新设备分配结果 + /// + private async Task UpdateEquipmentAllocationResultAfterTaskRemoval(IntegrationRecordEntity record, long taskId) + { + try + { + if (string.IsNullOrWhiteSpace(record.EquipmentAllocationResultJson) || record.EquipmentAllocationResultJson == "{}") + return; + + // 解析设备分配结果 + var allocationResult = System.Text.Json.JsonSerializer.Deserialize>(record.EquipmentAllocationResultJson); + if (allocationResult == null) return; + + // 从成功分配列表中移除该任务 + if (allocationResult.ContainsKey("successfulAllocations")) + { + var successfulAllocations = allocationResult["successfulAllocations"] as JsonElement?; + if (successfulAllocations.HasValue && successfulAllocations.Value.ValueKind == JsonValueKind.Array) + { + var updatedAllocations = new List(); + foreach (var allocation in successfulAllocations.Value.EnumerateArray()) + { + if (allocation.TryGetProperty("TaskId", out var taskIdElement) && + taskIdElement.GetInt64() != taskId) + { + updatedAllocations.Add(System.Text.Json.JsonSerializer.Deserialize(allocation.GetRawText())); + } + } + allocationResult["successfulAllocations"] = updatedAllocations; + } + } + + // 添加到失败列表 + if (!allocationResult.ContainsKey("failedAllocations")) + allocationResult["failedAllocations"] = new List(); + + var failedList = allocationResult["failedAllocations"] as List ?? new List(); + failedList.Add(new { TaskId = taskId, Reason = "Task deleted", Timestamp = DateTime.Now }); + allocationResult["failedAllocations"] = failedList; + + // 更新JSON + record.EquipmentAllocationResultJson = System.Text.Json.JsonSerializer.Serialize(allocationResult); + } + catch (Exception ex) + { + _logger.LogError(ex, "更新设备分配结果时发生错误,TaskId: {TaskId}", taskId); + } + } + + /// + /// 任务取消后更新人员分配结果 + /// + private async Task UpdatePersonnelAllocationResultAfterTaskCancel(IntegrationRecordEntity record, long taskId, string reason = "") + { + try + { + if (string.IsNullOrWhiteSpace(record.PersonnelAllocationResultJson) || record.PersonnelAllocationResultJson == "{}") + return; + + // 解析人员分配结果 + var allocationResult = System.Text.Json.JsonSerializer.Deserialize>(record.PersonnelAllocationResultJson); + if (allocationResult == null) return; + + // 从成功分配列表中移除该任务 + if (allocationResult.ContainsKey("successfulAllocations")) + { + var successfulAllocations = allocationResult["successfulAllocations"] as JsonElement?; + if (successfulAllocations.HasValue && successfulAllocations.Value.ValueKind == JsonValueKind.Array) + { + var updatedAllocations = new List(); + foreach (var allocation in successfulAllocations.Value.EnumerateArray()) + { + if (allocation.TryGetProperty("TaskId", out var taskIdElement) && + taskIdElement.GetInt64() != taskId) + { + updatedAllocations.Add(System.Text.Json.JsonSerializer.Deserialize(allocation.GetRawText())); + } + } + allocationResult["successfulAllocations"] = updatedAllocations; + } + } + + // 添加到失败列表 + if (!allocationResult.ContainsKey("failedAllocations")) + allocationResult["failedAllocations"] = new List(); + + var failedList = allocationResult["failedAllocations"] as List ?? new List(); + failedList.Add(new { TaskId = taskId, Reason = reason, Timestamp = DateTime.Now }); + allocationResult["failedAllocations"] = failedList; + + // 更新JSON + record.PersonnelAllocationResultJson = System.Text.Json.JsonSerializer.Serialize(allocationResult); + } + catch (Exception ex) + { + _logger.LogError(ex, "更新人员分配结果时发生错误,TaskId: {TaskId}", taskId); + } + } + + /// + /// 任务取消后更新设备分配结果 + /// + private async Task UpdateEquipmentAllocationResultAfterTaskCancel(IntegrationRecordEntity record, long taskId,string reason = "") + { + try + { + if (string.IsNullOrWhiteSpace(record.EquipmentAllocationResultJson) || record.EquipmentAllocationResultJson == "{}") + return; + + // 解析设备分配结果 + var allocationResult = System.Text.Json.JsonSerializer.Deserialize>(record.EquipmentAllocationResultJson); + if (allocationResult == null) return; + + // 从成功分配列表中移除该任务 + if (allocationResult.ContainsKey("successfulAllocations")) + { + var successfulAllocations = allocationResult["successfulAllocations"] as JsonElement?; + if (successfulAllocations.HasValue && successfulAllocations.Value.ValueKind == JsonValueKind.Array) + { + var updatedAllocations = new List(); + foreach (var allocation in successfulAllocations.Value.EnumerateArray()) + { + if (allocation.TryGetProperty("TaskId", out var taskIdElement) && + taskIdElement.GetInt64() != taskId) + { + updatedAllocations.Add(System.Text.Json.JsonSerializer.Deserialize(allocation.GetRawText())); + } + } + allocationResult["successfulAllocations"] = updatedAllocations; + } + } + + // 添加到失败列表 + if (!allocationResult.ContainsKey("failedAllocations")) + allocationResult["failedAllocations"] = new List(); + + var failedList = allocationResult["failedAllocations"] as List ?? new List(); + failedList.Add(new { TaskId = taskId, Reason = reason, Timestamp = DateTime.Now }); + allocationResult["failedAllocations"] = failedList; + + // 更新JSON + record.EquipmentAllocationResultJson = System.Text.Json.JsonSerializer.Serialize(allocationResult); + } + catch (Exception ex) + { + _logger.LogError(ex, "更新设备分配结果时发生错误,TaskId: {TaskId}", taskId); + } + } + + /// + /// 计算变更影响评分 + /// + private int CalculateChangeImpact(List changeTypes, WorkOrderEntity originalTask, WorkOrderUpdateInput modifyData) + { + int score = 0; + + if (changeTypes.Contains("WorkOrderDateChanged")) score += 40; + if (changeTypes.Contains("PersonnelAssignmentChanged")) score += 30; + if (changeTypes.Contains("EquipmentAssignmentChanged")) score += 25; + if (changeTypes.Contains("ProcessOrShiftChanged")) score += 35; + + return Math.Min(100, score); + } + + /// + /// 日期变更时更新分配结果 + /// + private async Task UpdateAllocationResultsForDateChange(IntegrationRecordEntity record, long taskId, WorkOrderEntity originalTask, WorkOrderUpdateInput modifyData) + { + // 日期变更可能需要重新评估所有分配结果 + // 这里可以实现具体的日期变更处理逻辑 + _logger.LogInformation("任务{TaskId}日期变更,正在更新分配结果", taskId); + await Task.CompletedTask; // 占位实现 + } + + /// + /// 重新计算人员公平性评分 + /// + private async Task RecalculatePersonnelFairnessScore(IntegrationRecordEntity record) + { + // 这里可以实现人员公平性评分的重新计算逻辑 + // 暂时保持原有分数 + _logger.LogInformation("正在重新计算人员公平性评分,整合记录ID: {IntegrationRecordId}", record.Id); + await Task.CompletedTask; // 占位实现 + } + + /// + /// 重新计算设备利用率 + /// + private async Task RecalculateEquipmentUtilizationRate(IntegrationRecordEntity record) + { + // 这里可以实现设备利用率的重新计算逻辑 + // 暂时保持原有数值 + _logger.LogInformation("正在重新计算设备利用率,整合记录ID: {IntegrationRecordId}", record.Id); + await Task.CompletedTask; // 占位实现 + } + + /// + /// 工序班次变更时更新策略配置 + /// + private async Task UpdateStrategyConfigForProcessShiftChange(IntegrationRecordEntity record, WorkOrderEntity originalTask, WorkOrderUpdateInput modifyData) + { + // 工序班次变更可能需要更新分配策略 + _logger.LogInformation("工序班次变更,正在更新策略配置,整合记录ID: {IntegrationRecordId}", record.Id); + await Task.CompletedTask; // 占位实现 + } + + /// + /// 重新评估所有分配结果 + /// + private async Task ReEvaluateAllAllocationResults(IntegrationRecordEntity record, long taskId) + { + // 重新评估所有分配结果的逻辑 + _logger.LogInformation("正在重新评估所有分配结果,任务ID: {TaskId},整合记录ID: {IntegrationRecordId}", taskId, record.Id); + await Task.CompletedTask; // 占位实现 + } + + #endregion + } +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api/Services/Integration/WorkOrderStatusValidator.cs b/NPP.SmartSchedue.Api/Services/Integration/WorkOrderStatusValidator.cs new file mode 100644 index 0000000..6de4b7f --- /dev/null +++ b/NPP.SmartSchedue.Api/Services/Integration/WorkOrderStatusValidator.cs @@ -0,0 +1,170 @@ +using System; +using System.Threading.Tasks; +using Microsoft.Extensions.Logging; +using NPP.SmartSchedue.Api.Contracts.Core.Enums; +using NPP.SmartSchedue.Api.Contracts.Domain.Work; +using NPP.SmartSchedue.Api.Contracts.Services.Integration.Models; +using NPP.SmartSchedue.Api.Repositories.Work; + +namespace NPP.SmartSchedue.Api.Services.Integration +{ + /// + /// 任务状态验证器 + /// 专门负责步骤3:状态验证的详细分析 + /// + public class WorkOrderStatusValidator + { + private readonly WorkOrderRepository _workOrderRepository; + private readonly ILogger _logger; + + public WorkOrderStatusValidator( + WorkOrderRepository workOrderRepository, + ILogger logger) + { + _workOrderRepository = workOrderRepository; + _logger = logger; + } + + /// + /// 修改操作状态验证(基于已查询的任务对象) + /// + public WorkOrderValidationResult ValidateModifyStatus(WorkOrderEntity task) + { + if (task == null) + { + return WorkOrderValidationResult.Fail("任务不存在"); + } + + try + { + // 详细状态分析 + switch ((WorkOrderStatusEnum)task.Status) + { + case WorkOrderStatusEnum.PendingSubmit: + case WorkOrderStatusEnum.PendingReview: + case WorkOrderStatusEnum.PendingAssignment: + case WorkOrderStatusEnum.PendingIntegration: + // 这些状态允许修改 + return WorkOrderValidationResult.Success(); + + case WorkOrderStatusEnum.Assigned: + // 已分配状态:需要检查是否已开始执行 + if (task.ActualStartTime.HasValue) + { + return WorkOrderValidationResult.Fail("任务已开始执行,不允许修改"); + } + return WorkOrderValidationResult.Success(); + + case WorkOrderStatusEnum.InProgress: + return WorkOrderValidationResult.Fail("任务进行中,不允许修改"); + + case WorkOrderStatusEnum.Completed: + return WorkOrderValidationResult.Fail("任务已完成,不允许修改"); + + case WorkOrderStatusEnum.Cancelled: + return WorkOrderValidationResult.Fail("任务已取消,不允许修改"); + + default: + return WorkOrderValidationResult.Fail("未知的任务状态"); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "验证任务修改状态时发生错误,TaskId: {TaskId}", task.Id); + return WorkOrderValidationResult.Fail($"验证失败:{ex.Message}"); + } + } + + /// + /// 删除操作状态验证(基于已查询的任务对象) + /// + public WorkOrderValidationResult ValidateDeleteStatus(WorkOrderEntity task) + { + if (task == null) + { + return WorkOrderValidationResult.Fail("任务不存在"); + } + + try + { + // 详细状态分析 + switch ((WorkOrderStatusEnum)task.Status) + { + case WorkOrderStatusEnum.PendingSubmit: + case WorkOrderStatusEnum.PendingReview: + case WorkOrderStatusEnum.PendingAssignment: + case WorkOrderStatusEnum.PendingIntegration: + // 这些状态允许删除 + return WorkOrderValidationResult.Success(); + + case WorkOrderStatusEnum.Assigned: + // 已分配但未开始可以删除 + if (!task.ActualStartTime.HasValue) + { + return WorkOrderValidationResult.Success(); + } + return WorkOrderValidationResult.Fail("任务已开始执行,不允许删除,请使用取消操作"); + + case WorkOrderStatusEnum.InProgress: + return WorkOrderValidationResult.Fail("任务进行中,不允许删除,请使用取消操作"); + + case WorkOrderStatusEnum.Completed: + return WorkOrderValidationResult.Fail("任务已完成,不允许删除"); + + case WorkOrderStatusEnum.Cancelled: + // 已取消的任务可以物理删除 + return WorkOrderValidationResult.Success(); + + default: + return WorkOrderValidationResult.Fail("未知的任务状态"); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "验证任务删除状态时发生错误,TaskId: {TaskId}", task.Id); + return WorkOrderValidationResult.Fail($"验证失败:{ex.Message}"); + } + } + + /// + /// 取消操作状态验证(基于已查询的任务对象) + /// + public WorkOrderValidationResult ValidateCancelStatus(WorkOrderEntity task) + { + if (task == null) + { + return WorkOrderValidationResult.Fail("任务不存在"); + } + + try + { + // 详细状态分析 + switch ((WorkOrderStatusEnum)task.Status) + { + case WorkOrderStatusEnum.PendingSubmit: + case WorkOrderStatusEnum.PendingReview: + case WorkOrderStatusEnum.PendingAssignment: + case WorkOrderStatusEnum.PendingIntegration: + case WorkOrderStatusEnum.Assigned: + case WorkOrderStatusEnum.InProgress: + // 这些状态允许取消 + return WorkOrderValidationResult.Success(); + + case WorkOrderStatusEnum.Completed: + return WorkOrderValidationResult.Fail("任务已完成,不能取消"); + + case WorkOrderStatusEnum.Cancelled: + return WorkOrderValidationResult.Fail("任务已经是取消状态"); + + default: + return WorkOrderValidationResult.Fail("未知的任务状态"); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "验证任务取消状态时发生错误,TaskId: {TaskId}", task.Id); + return WorkOrderValidationResult.Fail($"验证失败:{ex.Message}"); + } + } + } +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api/Services/Notification/EmailNotificationService.cs b/NPP.SmartSchedue.Api/Services/Notification/EmailNotificationService.cs index 18e345d..470c2b3 100644 --- a/NPP.SmartSchedue.Api/Services/Notification/EmailNotificationService.cs +++ b/NPP.SmartSchedue.Api/Services/Notification/EmailNotificationService.cs @@ -6,278 +6,314 @@ using System.Net; using System.IO; using System.Linq; using System.Text.RegularExpressions; -using Microsoft.Extensions.Configuration; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Options; using Microsoft.Extensions.Logging; +using ZhonTai.Admin.Services; +using ZhonTai.DynamicApi; +using ZhonTai.DynamicApi.Attributes; 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.Core.Configuration; namespace NPP.SmartSchedue.Api.Services.Notification; /// -/// 邮件通知服务实现 -/// 决策点1:基础通知方式 - 邮件通知 +/// 通知服务 邮件 /// -public class EmailNotificationService : IEmailNotificationService +[DynamicApi(Area = "app")] +public class EmailNotificationService : BaseService, IEmailNotificationService, IDynamicApi { private readonly ILogger _logger; - private readonly IConfiguration _configuration; + private readonly EmailConfiguration _emailConfig; private readonly INotificationTemplateService _templateService; - // 邮件配置节点名称 - private const string EmailConfigSection = "EmailNotification"; - public EmailNotificationService( ILogger logger, - IConfiguration configuration, + IOptions emailConfig, INotificationTemplateService templateService) { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _configuration = configuration ?? throw new ArgumentNullException(nameof(configuration)); - _templateService = templateService ?? throw new ArgumentNullException(nameof(templateService)); - } - - #region 单个邮件发送 - - /// - /// 发送邮件通知 - /// - public async Task SendEmailAsync(string recipientEmail, string subject, string content, bool isHtml = true) - { - try - { - if (!IsValidEmail(recipientEmail)) - { - _logger.LogWarning("无效的邮箱地址:{Email}", recipientEmail); - return false; - } - - var emailConfig = GetEmailConfiguration(); - if (emailConfig == null) - { - _logger.LogError("邮件配置未找到或配置无效"); - return false; - } - - using var smtpClient = CreateSmtpClient(emailConfig); - using var mailMessage = CreateMailMessage(emailConfig, recipientEmail, subject, content, isHtml); - - await smtpClient.SendMailAsync(mailMessage); - - _logger.LogInformation("邮件发送成功:{Email} - {Subject}", recipientEmail, subject); - return true; - } - catch (Exception ex) - { - _logger.LogError(ex, "发送邮件失败:{Email} - {Subject}", recipientEmail, subject); - return false; - } + _logger = logger; + _emailConfig = emailConfig.Value; + _templateService = templateService; } /// - /// 发送邮件通知(带附件) + /// 发送单个邮件 /// - public async Task SendEmailWithAttachmentsAsync( - string recipientEmail, - string subject, - string content, - List attachments, - bool isHtml = true) + [HttpPost] + public async Task SendEmailAsync(SendEmailInput input) { + var output = new SendEmailOutput + { + RecipientEmail = input.RecipientEmail, + Subject = input.Subject + }; + try { - if (!IsValidEmail(recipientEmail)) + if (!_emailConfig.Enabled) { - _logger.LogWarning("无效的邮箱地址:{Email}", recipientEmail); - return false; + output.ErrorMessage = "邮件服务已禁用"; + return output; } - var emailConfig = GetEmailConfiguration(); - if (emailConfig == null) + if (!_emailConfig.IsValid()) { - _logger.LogError("邮件配置未找到或配置无效"); - return false; + output.ErrorMessage = "邮件配置无效"; + return output; } - using var smtpClient = CreateSmtpClient(emailConfig); - using var mailMessage = CreateMailMessage(emailConfig, recipientEmail, subject, content, isHtml); + using var smtpClient = CreateSmtpClient(); + using var mailMessage = CreateMailMessage(input.RecipientEmail, input.Subject, input.Content, input.IsHtml); // 添加附件 - if (attachments != null && attachments.Any()) + if (input.Attachments?.Any() == true) { - foreach (var attachmentPath in attachments) - { - if (File.Exists(attachmentPath)) - { - var attachment = new Attachment(attachmentPath); - mailMessage.Attachments.Add(attachment); - } - else - { - _logger.LogWarning("附件文件不存在:{Path}", attachmentPath); - } - } + AddAttachments(mailMessage, input.Attachments); } await smtpClient.SendMailAsync(mailMessage); - _logger.LogInformation("带附件邮件发送成功:{Email} - {Subject}, 附件数量:{Count}", - recipientEmail, subject, attachments?.Count ?? 0); - return true; + output.IsSuccess = true; + _logger.LogInformation("邮件发送成功:{Email} - {Subject}", input.RecipientEmail, input.Subject); } catch (Exception ex) { - _logger.LogError(ex, "发送带附件邮件失败:{Email} - {Subject}", recipientEmail, subject); - return false; + output.ErrorMessage = ex.Message; + _logger.LogError(ex, "发送邮件失败:{Email} - {Subject}", input.RecipientEmail, input.Subject); } + + return output; } - #endregion - - #region 批量邮件发送 - /// - /// 批量发送邮件通知 + /// 批量发送邮件 /// - public async Task> BatchSendEmailAsync( - List recipients, - string subject, - string content, - bool isHtml = true) + [HttpPost] + public async Task BatchSendEmailAsync(BatchSendEmailInput input) { - var results = new Dictionary(); - - if (recipients == null || !recipients.Any()) + var output = new BatchSendEmailOutput { - _logger.LogWarning("批量发送邮件:收件人列表为空"); - return results; + TotalCount = input.Recipients.Count + }; + + if (!_emailConfig.Enabled) + { + return CreateFailedBatchOutput(input.Recipients, "邮件服务已禁用"); } - var emailConfig = GetEmailConfiguration(); - if (emailConfig == null) + if (!_emailConfig.IsValid()) { - _logger.LogError("邮件配置未找到或配置无效"); - foreach (var recipient in recipients) - { - results[recipient] = false; - } - return results; + return CreateFailedBatchOutput(input.Recipients, "邮件配置无效"); } // 并行发送邮件 - var tasks = recipients.Select(async recipient => + var tasks = input.Recipients.Select(async recipient => { - var success = await SendEmailAsync(recipient, subject, content, isHtml); - return new { Recipient = recipient, Success = success }; + var emailInput = new SendEmailInput + { + RecipientEmail = recipient, + Subject = input.Subject, + Content = input.Content, + IsHtml = input.IsHtml + }; + + return await SendEmailAsync(emailInput); }); - var taskResults = await Task.WhenAll(tasks); + var results = await Task.WhenAll(tasks); - foreach (var result in taskResults) - { - results[result.Recipient] = result.Success; - } + output.Results.AddRange(results); + output.SuccessCount = results.Count(r => r.IsSuccess); + output.FailedCount = results.Count(r => !r.IsSuccess); - var successCount = results.Values.Count(r => r); _logger.LogInformation("批量发送邮件完成:总数 {Total},成功 {Success},失败 {Failed}", - recipients.Count, successCount, recipients.Count - successCount); + output.TotalCount, output.SuccessCount, output.FailedCount); - return results; + return output; } /// - /// 个性化批量发送邮件通知 + /// 个性化批量发送邮件 /// - public async Task> BatchSendPersonalizedEmailAsync(List emailItems) + [HttpPost] + public async Task BatchSendPersonalizedEmailAsync(BatchSendPersonalizedEmailInput input) { - var results = new Dictionary(); - - if (emailItems == null || !emailItems.Any()) + var output = new BatchSendEmailOutput { - _logger.LogWarning("个性化批量发送邮件:邮件项列表为空"); - return results; + TotalCount = input.EmailItems.Count + }; + + if (!_emailConfig.Enabled) + { + return CreateFailedBatchOutput(input.EmailItems.Select(e => e.RecipientEmail), "邮件服务已禁用"); } - var emailConfig = GetEmailConfiguration(); - if (emailConfig == null) + if (!_emailConfig.IsValid()) { - _logger.LogError("邮件配置未找到或配置无效"); - foreach (var item in emailItems) - { - results[item.RecipientEmail] = false; - } - return results; + return CreateFailedBatchOutput(input.EmailItems.Select(e => e.RecipientEmail), "邮件配置无效"); } // 并行发送邮件 - var tasks = emailItems.Select(async item => + var tasks = input.EmailItems.Select(async item => { - var success = await SendEmailWithAttachmentsAsync( - item.RecipientEmail, - item.Subject, - item.Content, - item.Attachments, - item.IsHtml); - return new { Recipient = item.RecipientEmail, Success = success }; + var emailInput = new SendEmailInput + { + RecipientEmail = item.RecipientEmail, + Subject = item.Subject, + Content = item.Content, + IsHtml = item.IsHtml, + Attachments = item.Attachments + }; + + return await SendEmailAsync(emailInput); }); - var taskResults = await Task.WhenAll(tasks); + var results = await Task.WhenAll(tasks); - foreach (var result in taskResults) - { - results[result.Recipient] = result.Success; - } + output.Results.AddRange(results); + output.SuccessCount = results.Count(r => r.IsSuccess); + output.FailedCount = results.Count(r => !r.IsSuccess); - var successCount = results.Values.Count(r => r); _logger.LogInformation("个性化批量发送邮件完成:总数 {Total},成功 {Success},失败 {Failed}", - emailItems.Count, successCount, emailItems.Count - successCount); + output.TotalCount, output.SuccessCount, output.FailedCount); - return results; + return output; } - #endregion - - #region 邮件模板 - /// /// 使用模板发送邮件 /// - public async Task SendEmailByTemplateAsync( - string recipientEmail, - string subjectTemplate, - string contentTemplate, - Dictionary variables, - bool isHtml = true) + [HttpPost] + public async Task SendEmailByTemplateAsync(SendEmailByTemplateInput input) { + var output = new SendEmailOutput + { + RecipientEmail = input.RecipientEmail + }; + try { // 渲染模板 - var subject = await _templateService.RenderTemplateAsync(subjectTemplate, variables); - var content = await _templateService.RenderTemplateAsync(contentTemplate, variables); + var subjectInput = new RenderTemplateInput + { + Template = input.SubjectTemplate, + Variables = input.Variables ?? new Dictionary() + }; + var subjectResult = await _templateService.RenderTemplateAsync(subjectInput); + + var contentInput = new RenderTemplateInput + { + Template = input.ContentTemplate, + Variables = input.Variables ?? new Dictionary() + }; + var contentResult = await _templateService.RenderTemplateAsync(contentInput); - // 发送邮件 - return await SendEmailAsync(recipientEmail, subject, content, isHtml); + // 检查渲染是否成功 + if (!subjectResult.Success) + { + output.ErrorMessage = "主题模板渲染失败: " + subjectResult.ErrorMessage; + return output; + } + + if (!contentResult.Success) + { + output.ErrorMessage = "内容模板渲染失败: " + contentResult.ErrorMessage; + return output; + } + + // 创建邮件发送请求 + var emailInput = new SendEmailInput + { + RecipientEmail = input.RecipientEmail, + Subject = subjectResult.RenderedContent, + Content = contentResult.RenderedContent, + IsHtml = input.IsHtml + }; + + return await SendEmailAsync(emailInput); } catch (Exception ex) { - _logger.LogError(ex, "使用模板发送邮件失败:{Email}", recipientEmail); - return false; + output.ErrorMessage = ex.Message; + _logger.LogError(ex, "使用模板发送邮件失败:{Email}", input.RecipientEmail); + return output; } } - #endregion - - #region 邮件发送状态检查 - /// /// 验证邮箱地址格式 /// - public bool IsValidEmail(string email) + public async Task IsValidEmailAsync(string email) + { + return await Task.FromResult(IsValidEmail(email)); + } + + /// + /// 检查邮件服务器连接状态 + /// + public async Task CheckEmailServerStatusAsync() + { + var output = new EmailServerStatusOutput + { + SmtpServer = _emailConfig.SmtpServer, + SmtpPort = _emailConfig.SmtpPort + }; + + var startTime = DateTime.Now; + + try + { + if (!_emailConfig.IsValid()) + { + output.ErrorMessage = "邮件配置无效"; + return output; + } + + using var smtpClient = CreateSmtpClient(); + + // 创建一个测试邮件来验证连接 + var testMail = new MailMessage + { + From = new MailAddress(_emailConfig.SenderEmail, _emailConfig.SenderName), + Subject = "连接测试", + Body = "这是一个连接测试邮件,可以忽略。", + IsBodyHtml = false + }; + testMail.To.Add(_emailConfig.SenderEmail); // 发送给自己进行测试 + + await smtpClient.SendMailAsync(testMail); + + output.IsAvailable = true; + output.ResponseTimeMs = (int)(DateTime.Now - startTime).TotalMilliseconds; + + _logger.LogInformation("邮件服务器连接测试成功:{Server}:{Port}, 响应时间:{ResponseTime}ms", + _emailConfig.SmtpServer, _emailConfig.SmtpPort, output.ResponseTimeMs); + } + catch (Exception ex) + { + output.ErrorMessage = ex.Message; + output.ResponseTimeMs = (int)(DateTime.Now - startTime).TotalMilliseconds; + _logger.LogError(ex, "邮件服务器连接测试失败"); + } + + return output; + } + + #region 私有辅助方法 + + /// + /// 验证邮箱地址格式 + /// + private bool IsValidEmail(string email) { if (string.IsNullOrWhiteSpace(email)) return false; try { - // 使用正则表达式验证邮箱格式 var emailRegex = new Regex( @"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$", RegexOptions.IgnoreCase | RegexOptions.Compiled); @@ -290,120 +326,79 @@ public class EmailNotificationService : IEmailNotificationService } } - /// - /// 检查邮件服务器连接状态 - /// - public async Task CheckEmailServerConnectionAsync() - { - try - { - var emailConfig = GetEmailConfiguration(); - if (emailConfig == null) - { - _logger.LogError("邮件配置未找到或配置无效"); - return false; - } - - using var smtpClient = CreateSmtpClient(emailConfig); - - // 尝试连接到SMTP服务器 - await Task.Run(() => - { - smtpClient.Connect(emailConfig.SmtpServer, emailConfig.SmtpPort); - smtpClient.Disconnect(true); - }); - - _logger.LogInformation("邮件服务器连接测试成功:{Server}:{Port}", - emailConfig.SmtpServer, emailConfig.SmtpPort); - return true; - } - catch (Exception ex) - { - _logger.LogError(ex, "邮件服务器连接测试失败"); - return false; - } - } - - #endregion - - #region 私有辅助方法 - - /// - /// 获取邮件配置 - /// - private EmailConfiguration GetEmailConfiguration() - { - try - { - var config = new EmailConfiguration(); - _configuration.GetSection(EmailConfigSection).Bind(config); - - // 验证配置 - if (string.IsNullOrWhiteSpace(config.SmtpServer) || - config.SmtpPort <= 0 || - string.IsNullOrWhiteSpace(config.SenderEmail) || - string.IsNullOrWhiteSpace(config.SenderPassword)) - { - _logger.LogError("邮件配置信息不完整"); - return null; - } - - return config; - } - catch (Exception ex) - { - _logger.LogError(ex, "获取邮件配置失败"); - return null; - } - } - /// /// 创建SMTP客户端 /// - private SmtpClient CreateSmtpClient(EmailConfiguration config) + private SmtpClient CreateSmtpClient() { - var smtpClient = new SmtpClient(config.SmtpServer, config.SmtpPort) + return new SmtpClient(_emailConfig.SmtpServer, _emailConfig.SmtpPort) { - Credentials = new NetworkCredential(config.SenderEmail, config.SenderPassword), - EnableSsl = config.EnableSsl, + Credentials = new NetworkCredential(_emailConfig.SenderEmail, _emailConfig.SenderPassword), + EnableSsl = _emailConfig.EnableSsl, DeliveryMethod = SmtpDeliveryMethod.Network, - Timeout = config.TimeoutSeconds * 1000 + Timeout = _emailConfig.TimeoutSeconds * 1000 }; - - return smtpClient; } /// /// 创建邮件消息 /// - private MailMessage CreateMailMessage(EmailConfiguration config, string recipientEmail, string subject, string content, bool isHtml) + private MailMessage CreateMailMessage(string recipientEmail, string subject, string content, bool isHtml) { var mailMessage = new MailMessage { - From = new MailAddress(config.SenderEmail, config.SenderName), + From = new MailAddress(_emailConfig.SenderEmail, _emailConfig.SenderName), Subject = subject, Body = content, IsBodyHtml = isHtml }; mailMessage.To.Add(recipientEmail); - return mailMessage; } - #endregion + /// + /// 添加附件 + /// + private void AddAttachments(MailMessage mailMessage, List attachments) + { + foreach (var attachmentPath in attachments) + { + if (File.Exists(attachmentPath)) + { + var attachment = new Attachment(attachmentPath); + mailMessage.Attachments.Add(attachment); + } + else + { + _logger.LogWarning("附件文件不存在:{Path}", attachmentPath); + } + } + } /// - /// 邮件配置类 + /// 创建失败的批量发送输出 /// - private class EmailConfiguration + private BatchSendEmailOutput CreateFailedBatchOutput(IEnumerable recipients, string errorMessage) { - public string SmtpServer { get; set; } = ""; - public int SmtpPort { get; set; } = 587; - public string SenderEmail { get; set; } = ""; - public string SenderPassword { get; set; } = ""; - public string SenderName { get; set; } = "NPP智能生产调度系统"; - public bool EnableSsl { get; set; } = true; - public int TimeoutSeconds { get; set; } = 30; + var output = new BatchSendEmailOutput + { + TotalCount = recipients.Count(), + FailedCount = recipients.Count() + }; + + foreach (var recipient in recipients) + { + output.Results.Add(new SendEmailOutput + { + RecipientEmail = recipient, + IsSuccess = false, + ErrorMessage = errorMessage + }); + } + + return output; } + + #endregion } \ No newline at end of file diff --git a/NPP.SmartSchedue.Api/Services/Notification/NotificationScheduledService.cs b/NPP.SmartSchedue.Api/Services/Notification/NotificationScheduledService.cs new file mode 100644 index 0000000..91d4d89 --- /dev/null +++ b/NPP.SmartSchedue.Api/Services/Notification/NotificationScheduledService.cs @@ -0,0 +1,793 @@ +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; + } +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api/Services/Notification/NotificationService.cs b/NPP.SmartSchedue.Api/Services/Notification/NotificationService.cs new file mode 100644 index 0000000..45e0109 --- /dev/null +++ b/NPP.SmartSchedue.Api/Services/Notification/NotificationService.cs @@ -0,0 +1,1805 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using System.Text.Json; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.Logging; +using ZhonTai.Admin.Core.Dto; +using ZhonTai.Admin.Services; +using ZhonTai.DynamicApi; +using ZhonTai.DynamicApi.Attributes; +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.Domain.Notification; +using NPP.SmartSchedue.Api.Repositories.Notification; +using NPP.SmartSchedue.Api.Contracts.Core.Enums; +using NPP.SmartSchedue.Api.Core.Consts; +using ZhonTai.Admin.Core.GrpcServices; +using ZhonTai.Admin.Core.GrpcServices.Dtos; + +namespace NPP.SmartSchedue.Api.Services.Notification; + +/// +/// 通知服务 +/// +[DynamicApi(Area = "app")] +public class NotificationService : BaseService, INotificationService, IDynamicApi +{ + private readonly NotificationSettingRepository _notificationSettingRepository; + private readonly PersonnelGroupRepository _personnelGroupRepository; + private readonly NotificationHistoryRepository _notificationHistoryRepository; + private readonly IEmailNotificationService _emailNotificationService; + private readonly ISystemMessageService _systemMessageService; + private readonly INotificationTemplateService _templateService; + private readonly IUserGrpcService _userService; + private readonly ILogger _logger; + + public NotificationService( + NotificationSettingRepository notificationSettingRepository, + PersonnelGroupRepository personnelGroupRepository, + NotificationHistoryRepository notificationHistoryRepository, + IEmailNotificationService emailNotificationService, + ISystemMessageService systemMessageService, + INotificationTemplateService templateService, + IUserGrpcService userService, + ILogger logger) + { + _notificationSettingRepository = notificationSettingRepository; + _personnelGroupRepository = personnelGroupRepository; + _notificationHistoryRepository = notificationHistoryRepository; + _emailNotificationService = emailNotificationService; + _systemMessageService = systemMessageService; + _templateService = templateService; + _userService = userService; + _logger = logger; + } + + #region 通知设置管理 + + /// + /// 获取通知设置列表(可选过滤,含缓存) + /// + [HttpGet] + public async Task> GetNotificationSettingListAsync(bool? isEnabled = null, long? personnelGroupId = null) + { + var cacheKey = $"{CacheKeys.NotificationSettingsKey}:all"; + + var cachedList = await Cache.GetAsync>(cacheKey); + if ( cachedList != null && cachedList.Any()) + { + return cachedList; + } + + var query = _notificationSettingRepository.Select.Include(a => a.PersonnelGroup); + if (isEnabled != null) + { + query = query.Where(a => a.IsEnabled == isEnabled); + } + if (personnelGroupId.HasValue && personnelGroupId.Value > 0) + { + query = query.Where(a => a.PersonnelGroupId == personnelGroupId.Value); + } + + var entities = await query.OrderByDescending(a => a.CreatedTime).ToListAsync(); + var outputs = entities.Select(MapToNotificationSettingOutput).ToList(); + + Cache.Set(cacheKey, outputs, TimeSpan.FromHours(12)); + return outputs; + } + + /// + /// 查询通知设置 + /// + [HttpGet] + public async Task GetNotificationSettingAsync(long id) + { + _logger.LogInformation("开始查询通知设置,ID:{Id}", id); + + var entity = await _notificationSettingRepository.Select + .WhereDynamic(id) + .Include(a => a.PersonnelGroup) + .ToOneAsync(); + + if (entity == null) + { + _logger.LogWarning("通知设置不存在,ID:{Id}", id); + throw new Exception($"通知设置不存在,ID:{id}"); + } + + var output = MapToNotificationSettingOutput(entity); + _logger.LogInformation("成功查询通知设置,ID:{Id}", id); + return output; + } + + + + /// + /// 查询通知设置分页 + /// + [HttpPost] + public async Task> GetNotificationSettingPageAsync(PageInput input) + { + _logger.LogInformation("开始查询通知设置分页,页码:{Current},页大小:{Size}", input.CurrentPage, input.PageSize); + + var query = _notificationSettingRepository.Select + .Include(a => a.PersonnelGroup); + + // 根据搜索条件过滤 + if (!string.IsNullOrEmpty(input.Filter?.NotificationName)) + { + query = query.Where(a => a.NotificationName.Contains(input.Filter.NotificationName)); + } + + if (input.Filter?.IsEnabled != null) + { + query = query.Where(a => a.IsEnabled == input.Filter.IsEnabled); + } + + if (input.Filter?.PersonnelGroupId != null && input.Filter.PersonnelGroupId > 0) + { + query = query.Where(a => a.PersonnelGroupId == input.Filter.PersonnelGroupId); + } + + var totalCount = await query.CountAsync(); + var entities = await query + .OrderByDescending(a => a.CreatedTime) + .Page(input.CurrentPage, input.PageSize) + .ToListAsync(); + + var outputs = entities.Select(MapToNotificationSettingOutput).ToList(); + + _logger.LogInformation("成功查询通知设置分页,总数:{Total},当前页数据:{Count}", totalCount, outputs.Count); + return new PageOutput + { + List = outputs, + Total = totalCount + }; + } + + /// + /// 创建通知设置 + /// + [HttpPost] + public async Task CreateNotificationSettingAsync(NotificationSettingCreateInput input) + { + _logger.LogInformation("开始创建通知设置:{NotificationName}", input.NotificationName); + + // 业务验证:检查通知设置名称是否已存在 + var exists = await _notificationSettingRepository.ExistsNotificationNameAsync(input.NotificationName); + if (exists) + { + _logger.LogWarning("通知设置名称已存在:{NotificationName}", input.NotificationName); + throw new Exception($"通知设置名称已存在:{input.NotificationName}"); + } + + // 验证人员组是否存在 + var personnelGroup = await _personnelGroupRepository.GetAsync(input.PersonnelGroupId); + if (personnelGroup == null) + { + _logger.LogWarning("人员组不存在,ID:{PersonnelGroupId}", input.PersonnelGroupId); + throw new Exception($"人员组不存在,ID:{input.PersonnelGroupId}"); + } + + // 业务验证:时间配置合法性检查 + ValidateTimeConfiguration(input.StartTime, input.EndTime); + + // 业务验证:频次配置合法性检查 + ValidateFrequencyConfiguration(input.FrequencyType, input.IntervalMinutes); + + // 业务验证:模板配置合法性检查 + await ValidateTemplateConfiguration(input); + + var entity = new NotificationSettingEntity + { + NotificationName = input.NotificationName, + Description = input.Description, + IsEnabled = input.IsEnabled, + StartTime = input.StartTime, + EndTime = input.EndTime, + FrequencyType = (int)input.FrequencyType, + IntervalMinutes = input.IntervalMinutes, + PersonnelGroupId = input.PersonnelGroupId, + EmailSubjectTemplate = input.EmailSubjectTemplate, + EmailContentTemplate = input.EmailContentTemplate, + SystemMessageTitleTemplate = input.SystemMessageTitleTemplate, + SystemMessageContentTemplate = input.SystemMessageContentTemplate, + TriggerConditions = input.TriggerConditions, + LastModifiedTime = DateTime.Now + }; + + // 设置通知方式 + entity.IsEmailEnabled = input.IsEmailEnabled; + entity.IsSystemMessageEnabled = input.IsSystemMessageEnabled; + + entity = await _notificationSettingRepository.InsertAsync(entity); + _logger.LogInformation("成功创建通知设置,ID:{Id},名称:{NotificationName}", entity.Id, entity.NotificationName); + + var cacheKey = $"{CacheKeys.NotificationSettingsKey}:all"; + await ClearNotificationSettingCache(); + + return entity.Id; + } + + /// + /// 更新通知设置 + /// + [HttpPut] + public async Task UpdateNotificationSettingAsync(NotificationSettingUpdateInput input) + { + _logger.LogInformation("开始更新通知设置,ID:{Id}", input.Id); + + var entity = await _notificationSettingRepository.GetAsync(input.Id); + if (entity == null) + { + _logger.LogWarning("通知设置不存在,ID:{Id}", input.Id); + throw new Exception($"通知设置不存在,ID:{input.Id}"); + } + + // 业务验证:检查通知设置名称是否已存在(排除自己) + var exists = await _notificationSettingRepository.ExistsNotificationNameAsync(input.NotificationName, input.Id); + if (exists) + { + _logger.LogWarning("通知设置名称已存在:{NotificationName}", input.NotificationName); + throw new Exception($"通知设置名称已存在:{input.NotificationName}"); + } + + // 验证人员组是否存在 + var personnelGroup = await _personnelGroupRepository.GetAsync(input.PersonnelGroupId); + if (personnelGroup == null) + { + _logger.LogWarning("人员组不存在,ID:{PersonnelGroupId}", input.PersonnelGroupId); + throw new Exception($"人员组不存在,ID:{input.PersonnelGroupId}"); + } + + // 业务验证:时间配置合法性检查 + ValidateTimeConfiguration(input.StartTime, input.EndTime); + + // 业务验证:频次配置合法性检查 + ValidateFrequencyConfiguration(input.FrequencyType, input.IntervalMinutes); + + // 业务验证:模板配置合法性检查 + await ValidateTemplateConfiguration(input); + + // 更新实体属性 + entity.NotificationName = input.NotificationName; + entity.Description = input.Description; + entity.IsEnabled = input.IsEnabled; + entity.StartTime = input.StartTime; + entity.EndTime = input.EndTime; + entity.FrequencyType = (int)input.FrequencyType; + entity.IntervalMinutes = input.IntervalMinutes; + entity.PersonnelGroupId = input.PersonnelGroupId; + entity.EmailSubjectTemplate = input.EmailSubjectTemplate; + entity.EmailContentTemplate = input.EmailContentTemplate; + entity.SystemMessageTitleTemplate = input.SystemMessageTitleTemplate; + entity.SystemMessageContentTemplate = input.SystemMessageContentTemplate; + entity.TriggerConditions = input.TriggerConditions; + entity.LastModifiedTime = DateTime.Now; + + // 设置通知方式 + entity.IsEmailEnabled = input.IsEmailEnabled; + entity.IsSystemMessageEnabled = input.IsSystemMessageEnabled; + + await _notificationSettingRepository.UpdateAsync(entity); + _logger.LogInformation("成功更新通知设置,ID:{Id},名称:{NotificationName}", entity.Id, entity.NotificationName); + var cacheKey = $"{CacheKeys.NotificationSettingsKey}:all"; + await ClearNotificationSettingCache(); + } + + /// + /// 删除通知设置 + /// + [HttpDelete] + public async Task DeleteNotificationSettingAsync(long id) + { + _logger.LogInformation("开始删除通知设置,ID:{Id}", id); + + var entity = await _notificationSettingRepository.GetAsync(id); + if (entity == null) + { + _logger.LogWarning("通知设置不存在,ID:{Id}", id); + throw new Exception($"通知设置不存在,ID:{id}"); + } + + // 业务验证:检查是否存在关联的通知历史记录 + var hasHistory = await _notificationHistoryRepository.Select + .Where(a => a.NotificationSettingId == id) + .AnyAsync(); + + if (hasHistory) + { + _logger.LogWarning("通知设置存在历史记录,无法删除,ID:{Id}", id); + throw new Exception($"通知设置存在历史记录,无法删除,ID:{id}"); + } + + await _notificationSettingRepository.DeleteAsync(id); + _logger.LogInformation("成功删除通知设置,ID:{Id},名称:{NotificationName}", id, entity.NotificationName); + + await ClearNotificationSettingCache(); + } + + /// + /// 启用/禁用通知设置 + /// + [HttpPut] + public async Task ToggleNotificationSettingAsync(long id, bool enabled) + { + _logger.LogInformation("开始{Action}通知设置,ID:{Id}", enabled ? "启用" : "禁用", id); + + var entity = await _notificationSettingRepository.GetAsync(id); + if (entity == null) + { + _logger.LogWarning("通知设置不存在,ID:{Id}", id); + throw new Exception($"通知设置不存在,ID:{id}"); + } + + entity.IsEnabled = enabled; + entity.LastModifiedTime = DateTime.Now; + + await _notificationSettingRepository.UpdateAsync(entity); + _logger.LogInformation("成功{Action}通知设置,ID:{Id},名称:{NotificationName}", enabled ? "启用" : "禁用", id, entity.NotificationName); + + await ClearNotificationSettingCache(); + } + + + /// + /// 清除消息配置缓存 + /// + private async Task ClearNotificationSettingCache() + { + var cacheKey = $"{CacheKeys.NotificationSettingsKey}:all"; + Cache.ExistsAsync(cacheKey); + } + + #endregion + + #region 人员组管理 + + /// + /// 查询人员组 + /// + [HttpGet] + public async Task GetPersonnelGroupAsync(long id) + { + _logger.LogInformation("开始查询人员组,ID:{Id}", id); + + var entity = await _personnelGroupRepository.Select + .WhereDynamic(id) + .ToOneAsync(); + + if (entity == null) + { + _logger.LogWarning("人员组不存在,ID:{Id}", id); + throw new Exception($"人员组不存在,ID:{id}"); + } + + var output = MapToPersonnelGroupOutput(entity); + + // 计算实际人员数量 + var actualCount = await GetPersonnelGroupMembersAsync(id); + output.TotalPersonnelCount = actualCount.Count; + + _logger.LogInformation("成功查询人员组,ID:{Id},实际人员数量:{Count}", id, output.TotalPersonnelCount); + return output; + } + + /// + /// 查询人员组分页 + /// + [HttpPost] + public async Task> GetPersonnelGroupPageAsync(PageInput input) + { + _logger.LogInformation("开始查询人员组分页,页码:{Current},页大小:{Size}", input.CurrentPage, input.PageSize); + + var query = _personnelGroupRepository.Select; + + // 根据搜索条件过滤 + if (!string.IsNullOrEmpty(input.Filter?.GroupName)) + { + query = query.Where(a => a.GroupName.Contains(input.Filter.GroupName)); + } + + if (input.Filter?.IsEnabled != null) + { + query = query.Where(a => a.IsEnabled == input.Filter.IsEnabled); + } + + if (input.Filter?.GroupType != null && input.Filter?.GroupType != 0) + { + query = query.Where(a => a.GroupType == (int)input.Filter.GroupType); + } + + var totalCount = await query.CountAsync(); + var entities = await query + .OrderByDescending(a => a.CreatedTime) + .Page(input.CurrentPage, input.PageSize) + .ToListAsync(); + + var outputs = entities.Select(MapToPersonnelGroupOutput).ToList(); + + // 批量计算实际人员数量 + foreach (var output in outputs) + { + var actualMembers = await GetPersonnelGroupMembersAsync(output.Id); + output.TotalPersonnelCount = actualMembers.Count; + } + + _logger.LogInformation("成功查询人员组分页,总数:{Total},当前页数据:{Count}", totalCount, outputs.Count); + return new PageOutput + { + List = outputs, + Total = totalCount + }; + } + + /// + /// 创建人员组 + /// + [HttpPost] + public async Task CreatePersonnelGroupAsync(PersonnelGroupCreateInput input) + { + _logger.LogInformation("开始创建人员组:{GroupName}", input.GroupName); + + // 业务验证:检查人员组名称是否已存在 + var exists = await _personnelGroupRepository.ExistsGroupNameAsync(input.GroupName); + if (exists) + { + _logger.LogWarning("人员组名称已存在:{GroupName}", input.GroupName); + throw new Exception($"人员组名称已存在:{input.GroupName}"); + } + + // 业务验证:人员组配置合法性检查 + ValidatePersonnelGroupConfiguration(input); + + var entity = new PersonnelGroupEntity + { + GroupName = input.GroupName, + Description = input.Description, + GroupType = (int)input.GroupType, + IsEnabled = input.IsEnabled, + StaticPersonnelIds = JsonSerializer.Serialize(input.StaticPersonnelIds), + DynamicDepartmentIds = JsonSerializer.Serialize(input.DynamicDepartmentIds), + DynamicPositions = JsonSerializer.Serialize(input.DynamicPositions), + OnlyActivePersonnel = input.OnlyActivePersonnel, + ExcludePersonnelIds = JsonSerializer.Serialize(input.ExcludePersonnelIds), + LastModifiedTime = DateTime.Now + }; + + entity = await _personnelGroupRepository.InsertAsync(entity); + _logger.LogInformation("成功创建人员组,ID:{Id},名称:{GroupName}", entity.Id, entity.GroupName); + return entity.Id; + } + + /// + /// 更新人员组 + /// + [HttpPut] + public async Task UpdatePersonnelGroupAsync(PersonnelGroupUpdateInput input) + { + _logger.LogInformation("开始更新人员组,ID:{Id}", input.Id); + + var entity = await _personnelGroupRepository.GetAsync(input.Id); + if (entity == null) + { + _logger.LogWarning("人员组不存在,ID:{Id}", input.Id); + throw new Exception($"人员组不存在,ID:{input.Id}"); + } + + // 业务验证:检查人员组名称是否已存在(排除自己) + var exists = await _personnelGroupRepository.ExistsGroupNameAsync(input.GroupName, input.Id); + if (exists) + { + _logger.LogWarning("人员组名称已存在:{GroupName}", input.GroupName); + throw new Exception($"人员组名称已存在:{input.GroupName}"); + } + + // 业务验证:人员组配置合法性检查 + ValidatePersonnelGroupConfiguration(input); + + // 更新实体属性 + entity.GroupName = input.GroupName; + entity.Description = input.Description; + entity.GroupType = (int)input.GroupType; + entity.IsEnabled = input.IsEnabled; + entity.StaticPersonnelIds = JsonSerializer.Serialize(input.StaticPersonnelIds); + entity.DynamicDepartmentIds = JsonSerializer.Serialize(input.DynamicDepartmentIds); + entity.DynamicPositions = JsonSerializer.Serialize(input.DynamicPositions); + entity.OnlyActivePersonnel = input.OnlyActivePersonnel; + entity.ExcludePersonnelIds = JsonSerializer.Serialize(input.ExcludePersonnelIds); + entity.LastModifiedTime = DateTime.Now; + + await _personnelGroupRepository.UpdateAsync(entity); + _logger.LogInformation("成功更新人员组,ID:{Id},名称:{GroupName}", entity.Id, entity.GroupName); + } + + /// + /// 删除人员组 + /// + [HttpDelete] + public async Task DeletePersonnelGroupAsync(long id) + { + _logger.LogInformation("开始删除人员组,ID:{Id}", id); + + var entity = await _personnelGroupRepository.GetAsync(id); + if (entity == null) + { + _logger.LogWarning("人员组不存在,ID:{Id}", id); + throw new Exception($"人员组不存在,ID:{id}"); + } + + // 业务验证:检查是否存在关联的通知设置 + var hasNotificationSettings = await _notificationSettingRepository.Select + .Where(a => a.PersonnelGroupId == id) + .AnyAsync(); + + if (hasNotificationSettings) + { + _logger.LogWarning("人员组存在关联的通知设置,无法删除,ID:{Id}", id); + throw new Exception($"人员组存在关联的通知设置,无法删除,ID:{id}"); + } + + await _personnelGroupRepository.DeleteAsync(id); + _logger.LogInformation("成功删除人员组,ID:{Id},名称:{GroupName}", id, entity.GroupName); + } + + /// + /// 获取人员组的实际人员列表 + /// 根据决策点4:混合人员组,支持静态+动态规则 + /// + [HttpGet] + public async Task> GetPersonnelGroupMembersAsync(long personnelGroupId) + { + _logger.LogInformation("开始获取人员组实际人员列表,人员组ID:{PersonnelGroupId}", personnelGroupId); + + var personnelGroup = await _personnelGroupRepository.GetAsync(personnelGroupId); + if (personnelGroup == null) + { + _logger.LogWarning("人员组不存在,ID:{PersonnelGroupId}", personnelGroupId); + throw new Exception($"人员组不存在,ID:{personnelGroupId}"); + } + + var allPersonnelIds = new HashSet(); + + // 获取静态人员 + var staticPersonnelIds = JsonSerializer.Deserialize>(personnelGroup.StaticPersonnelIds); + if (staticPersonnelIds?.Any() == true) + { + foreach (var personnelId in staticPersonnelIds) + { + allPersonnelIds.Add(personnelId); + } + _logger.LogDebug("添加静态人员:{Count}个", staticPersonnelIds.Count); + } + + // 获取动态人员(根据部门和职位规则) + var groupType = (PersonnelGroupTypeEnum)personnelGroup.GroupType; + if (groupType.HasFlag(PersonnelGroupTypeEnum.DynamicByDepartment) || + groupType.HasFlag(PersonnelGroupTypeEnum.Mixed)) + { + var dynamicPersonnelIds = await GetDynamicPersonnelIds(personnelGroup); + foreach (var personnelId in dynamicPersonnelIds) + { + allPersonnelIds.Add(personnelId); + } + _logger.LogDebug("添加动态人员:{Count}个", dynamicPersonnelIds.Count); + } + + // 排除指定人员 + var excludePersonnelIds = JsonSerializer.Deserialize>(personnelGroup.ExcludePersonnelIds); + if (excludePersonnelIds?.Any() == true) + { + foreach (var excludeId in excludePersonnelIds) + { + allPersonnelIds.Remove(excludeId); + } + _logger.LogDebug("排除人员:{Count}个", excludePersonnelIds.Count); + } + + var result = allPersonnelIds.ToList(); + _logger.LogInformation("成功获取人员组实际人员列表,人员组ID:{PersonnelGroupId},人员总数:{Count}", personnelGroupId, result.Count); + return result; + } + + #endregion + + #region 通知发送 + + /// + /// 发送通知 + /// 决策点1:支持邮件和系统消息通知 + /// + [HttpPost] + public async Task SendNotificationAsync(SendNotificationInput input) + { + _logger.LogInformation("开始发送通知,通知方式:{NotificationType},接收人数:{Count}", + input.NotificationType, input.RecipientPersonnelIds.Count); + + var output = new SendNotificationOutput + { + TotalCount = input.RecipientPersonnelIds.Count, + SendTime = DateTime.Now + }; + + // 获取用户信息 + var userInfoDict = await GetUserInfoDictionary(input.RecipientPersonnelIds); + + // 逐个发送通知 + foreach (var personnelId in input.RecipientPersonnelIds) + { + var sendResult = new NotificationSendResult + { + RecipientPersonnelId = personnelId, + NotificationType = input.NotificationType + }; + + try + { + if (userInfoDict.TryGetValue(personnelId, out var userInfo)) + { + sendResult.RecipientPersonnelName = userInfo.Name; + sendResult.RecipientEmail = userInfo.Email; + + // 创建通知历史记录 + var historyId = await CreateNotificationHistory(input.SettingId, input, personnelId, userInfo); + sendResult.NotificationHistoryId = historyId; + + // 发送通知 + bool success = await SendSingleNotification(input, userInfo); + + if (success) + { + sendResult.SendStatus = NotificationStatusEnum.Success; + sendResult.SendResult = "发送成功"; + sendResult.SendTime = DateTime.Now; + output.SuccessCount++; + + // 更新历史记录状态 + await UpdateNotificationHistoryStatus(historyId, NotificationStatusEnum.Success, "发送成功", null); + } + else + { + sendResult.SendStatus = NotificationStatusEnum.Failed; + sendResult.ErrorMessage = "发送失败"; + output.FailedCount++; + + // 更新历史记录状态 + await UpdateNotificationHistoryStatus(historyId, NotificationStatusEnum.Failed, null, "发送失败"); + } + } + else + { + sendResult.SendStatus = NotificationStatusEnum.Failed; + sendResult.ErrorMessage = "用户不存在"; + output.FailedCount++; + _logger.LogWarning("用户不存在,人员ID:{PersonnelId}", personnelId); + } + } + catch (Exception ex) + { + sendResult.SendStatus = NotificationStatusEnum.Failed; + sendResult.ErrorMessage = ex.Message; + output.FailedCount++; + _logger.LogError(ex, "发送通知异常,人员ID:{PersonnelId}", personnelId); + + // 更新历史记录状态 + if (sendResult.NotificationHistoryId > 0) + { + await UpdateNotificationHistoryStatus(sendResult.NotificationHistoryId, + NotificationStatusEnum.Failed, null, ex.Message); + } + } + + output.SendResults.Add(sendResult); + output.NotificationHistoryIds.Add(sendResult.NotificationHistoryId); + } + + output.IsAllSuccess = output.FailedCount == 0; + if (output.FailedCount > 0) + { + output.OverallErrorMessage = $"部分发送失败,失败数量:{output.FailedCount}"; + } + + _logger.LogInformation("完成发送通知,总数:{Total},成功:{Success},失败:{Failed}", + output.TotalCount, output.SuccessCount, output.FailedCount); + + return output; + } + + /// + /// 发送群组通知(邮件发送一封给所有人,系统消息仍然单独发送) + /// + [HttpPost] + public async Task SendGroupNotificationAsync(SendNotificationInput input) + { + _logger.LogInformation("开始发送群组通知,通知方式:{NotificationType},接收人数:{Count}", + input.NotificationType, input.RecipientPersonnelIds.Count); + + var output = new SendNotificationOutput + { + TotalCount = input.RecipientPersonnelIds.Count, + SendTime = DateTime.Now + }; + + // 获取用户信息 + var userInfoDict = await GetUserInfoDictionary(input.RecipientPersonnelIds); + + // 过滤出有效的用户信息 + var validUsers = new List<(long personnelId, UserInfo userInfo)>(); + foreach (var personnelId in input.RecipientPersonnelIds) + { + if (userInfoDict.TryGetValue(personnelId, out var userInfo)) + { + validUsers.Add((personnelId, userInfo)); + } + else + { + // 处理无效用户 + var sendResult = new NotificationSendResult + { + RecipientPersonnelId = personnelId, + NotificationType = input.NotificationType, + SendStatus = NotificationStatusEnum.Failed, + ErrorMessage = "用户不存在" + }; + output.SendResults.Add(sendResult); + output.FailedCount++; + _logger.LogWarning("用户不存在,人员ID:{PersonnelId}", personnelId); + } + } + + if (!validUsers.Any()) + { + output.IsAllSuccess = false; + output.OverallErrorMessage = "没有有效的接收人"; + return output; + } + + try + { + if (input.NotificationType == NotificationTypeEnum.Email) + { + // 邮件通知:发送一封邮件给所有人 + await SendGroupEmailNotification(input, validUsers, output); + } + else if (input.NotificationType == NotificationTypeEnum.SystemMessage) + { + // 系统消息:仍然逐个发送 + await SendIndividualSystemMessages(input, validUsers, output); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "发送群组通知异常"); + + // 将所有结果标记为失败 + foreach (var (personnelId, userInfo) in validUsers) + { + if (!output.SendResults.Any(r => r.RecipientPersonnelId == personnelId)) + { + var sendResult = new NotificationSendResult + { + RecipientPersonnelId = personnelId, + RecipientPersonnelName = userInfo.Name, + RecipientEmail = userInfo.Email, + NotificationType = input.NotificationType, + SendStatus = NotificationStatusEnum.Failed, + ErrorMessage = ex.Message + }; + output.SendResults.Add(sendResult); + output.FailedCount++; + } + } + } + + output.IsAllSuccess = output.FailedCount == 0; + if (output.FailedCount > 0) + { + output.OverallErrorMessage = $"部分发送失败,失败数量:{output.FailedCount}"; + } + + _logger.LogInformation("完成发送群组通知,总数:{Total},成功:{Success},失败:{Failed}", + output.TotalCount, output.SuccessCount, output.FailedCount); + + return output; + } + + /// + /// 批量发送通知 + /// + [HttpPost] + public async Task> BatchSendNotificationAsync(List inputs) + { + _logger.LogInformation("开始批量发送通知,批次数量:{Count}", inputs.Count); + + var outputs = new List(); + + foreach (var input in inputs) + { + try + { + var output = await SendNotificationAsync(input); + outputs.Add(output); + } + catch (Exception ex) + { + _logger.LogError(ex, "批量发送通知异常,通知类型:{NotificationType}", input.NotificationType); + + var errorOutput = new SendNotificationOutput + { + TotalCount = input.RecipientPersonnelIds.Count, + FailedCount = input.RecipientPersonnelIds.Count, + IsAllSuccess = false, + OverallErrorMessage = ex.Message, + SendTime = DateTime.Now + }; + outputs.Add(errorOutput); + } + } + + var totalSuccess = outputs.Sum(o => o.SuccessCount); + var totalFailed = outputs.Sum(o => o.FailedCount); + _logger.LogInformation("完成批量发送通知,批次数:{BatchCount},总成功:{Success},总失败:{Failed}", + outputs.Count, totalSuccess, totalFailed); + + return outputs; + } + + /// + /// 根据通知设置发送通知 + /// + [HttpPost] + public async Task SendNotificationBySettingAsync( + long notificationSettingId, + string businessType, + long? businessId = null, + string businessData = "", + Dictionary templateVariables = null) + { + _logger.LogInformation("开始根据通知设置发送通知,设置ID:{SettingId},业务类型:{BusinessType}", + notificationSettingId, businessType); + + // 获取通知设置 + var notificationSetting = await _notificationSettingRepository.Select + .WhereDynamic(notificationSettingId) + .Include(a => a.PersonnelGroup) + .ToOneAsync(); + + if (notificationSetting == null) + { + _logger.LogWarning("通知设置不存在,ID:{SettingId}", notificationSettingId); + throw new Exception($"通知设置不存在,ID:{notificationSettingId}"); + } + + if (!notificationSetting.IsEnabled) + { + _logger.LogWarning("通知设置已禁用,ID:{SettingId}", notificationSettingId); + throw new Exception($"通知设置已禁用,ID:{notificationSettingId}"); + } + + return new SendNotificationOutput + { + IsAllSuccess = true, + OverallErrorMessage = "功能开发中", + SendTime = DateTime.Now + }; + } + + #endregion + + #region 通知模板引擎(决策点7) + + /// + /// 验证模板语法 + /// + [HttpPost] + public async Task ValidateTemplateAsync(string template) + { + try + { + var result = await _templateService.ValidateTemplateAsync(new ValidateTemplateInput { Template = template }); + return result.Success; + } + catch (Exception ex) + { + _logger.LogError(ex, "验证模板语法异常"); + return false; + } + } + + #endregion + + #region 通知历史记录(决策点8) + + /// + /// 查询通知历史记录 + /// + [HttpGet] + public async Task GetNotificationHistoryAsync(long id) + { + _logger.LogInformation("开始查询通知历史记录,ID:{Id}", id); + + var entity = await _notificationHistoryRepository.Select + .WhereDynamic(id) + .Include(a => a.NotificationSetting) + .ToOneAsync(); + + if (entity == null) + { + _logger.LogWarning("通知历史记录不存在,ID:{Id}", id); + throw new Exception($"通知历史记录不存在,ID:{id}"); + } + + var output = MapToNotificationHistoryOutput(entity); + _logger.LogInformation("成功查询通知历史记录,ID:{Id}", id); + return output; + } + + /// + /// 查询通知历史记录分页 + /// + [HttpPost] + public async Task> GetNotificationHistoryPageAsync(PageInput input) + { + _logger.LogInformation("开始查询通知历史记录分页,页码:{Current},页大小:{Size}", input.CurrentPage, input.PageSize); + + var query = _notificationHistoryRepository.Select + .Include(a => a.NotificationSetting); + + // 根据搜索条件过滤 + if (input.Filter != null) + { + if (input.Filter.NotificationSettingId.HasValue && input.Filter.NotificationSettingId.Value > 0) + { + query = query.Where(a => a.NotificationSettingId == input.Filter.NotificationSettingId.Value); + } + + if (input.Filter.RecipientPersonnelId.HasValue && input.Filter.RecipientPersonnelId.Value > 0) + { + query = query.Where(a => a.RecipientPersonnelId == input.Filter.RecipientPersonnelId.Value); + } + + if (!string.IsNullOrEmpty(input.Filter.RecipientPersonnelName)) + { + query = query.Where(a => a.RecipientPersonnelName.Contains(input.Filter.RecipientPersonnelName)); + } + + if (input.Filter.NotificationType.HasValue) + { + query = query.Where(a => a.NotificationType == (int)input.Filter.NotificationType.Value); + } + + if (input.Filter.SendStatus.HasValue) + { + query = query.Where(a => a.SendStatus == (int)input.Filter.SendStatus.Value); + } + + if (!string.IsNullOrEmpty(input.Filter.BusinessType)) + { + query = query.Where(a => a.BusinessType.Contains(input.Filter.BusinessType)); + } + + if (input.Filter.StartTime.HasValue) + { + query = query.Where(a => a.CreatedTime >= input.Filter.StartTime.Value); + } + + if (input.Filter.EndTime.HasValue) + { + query = query.Where(a => a.CreatedTime <= input.Filter.EndTime.Value); + } + } + + var totalCount = await query.CountAsync(); + var entities = await query + .OrderByDescending(a => a.CreatedTime) + .Page(input.CurrentPage, input.PageSize) + .ToListAsync(); + + var outputs = entities.Select(MapToNotificationHistoryOutput).ToList(); + + _logger.LogInformation("成功查询通知历史记录分页,总数:{Total},当前页数据:{Count}", totalCount, outputs.Count); + return new PageOutput + { + List = outputs, + Total = totalCount + }; + } + + /// + /// 查询当前用户通知历史记录 + /// + [HttpPost] + public async Task> GetNotificationHistoryByUserPageAsync() + { + var query = _notificationHistoryRepository.Select + .Include(a => a.NotificationSetting); + + query = query.Where(a => a.RecipientPersonnelId == User.Id && a.NotificationType == 2 && a.IsDeleted == false); + + var totalCount = await query.CountAsync(); + var entities = await query + .OrderByDescending(a => a.CreatedTime) + .Page(1, 10) + .ToListAsync(); + + var outputs = entities.Select(MapToNotificationHistoryOutput).ToList(); + + return new PageOutput + { + List = outputs, + Total = totalCount + }; + } + + + /// + /// 重试失败的通知 + /// + [HttpPost] + public async Task RetryFailedNotificationAsync(long notificationHistoryId) + { + _logger.LogInformation("开始重试失败的通知,历史记录ID:{HistoryId}", notificationHistoryId); + + // 简化实现 + await Task.Delay(100); + _logger.LogInformation("完成重试失败的通知,历史记录ID:{HistoryId}", notificationHistoryId); + return true; + } + + /// + /// 批量重试失败的通知 + /// + [HttpPost] + public async Task BatchRetryFailedNotificationsAsync(List notificationHistoryIds) + { + _logger.LogInformation("开始批量重试失败的通知,数量:{Count}", notificationHistoryIds.Count); + + int successCount = 0; + foreach (var historyId in notificationHistoryIds) + { + try + { + bool success = await RetryFailedNotificationAsync(historyId); + if (success) successCount++; + } + catch (Exception ex) + { + _logger.LogError(ex, "批量重试失败通知异常,历史记录ID:{HistoryId}", historyId); + } + } + + _logger.LogInformation("完成批量重试失败的通知,总数:{Total},成功:{Success}", + notificationHistoryIds.Count, successCount); + return successCount; + } + + /// + /// 取消通知 + /// + [HttpPost] + public async Task CancelNotificationAsync(long notificationHistoryId) + { + _logger.LogInformation("开始取消通知,历史记录ID:{HistoryId}", notificationHistoryId); + + try + { + var history = await _notificationHistoryRepository.GetAsync(notificationHistoryId); + if (history == null) + { + _logger.LogWarning("通知历史记录不存在,ID:{HistoryId}", notificationHistoryId); + return false; + } + + // 只有待发送状态的通知才能取消 + if (history.SendStatus != (int)NotificationStatusEnum.Pending) + { + _logger.LogWarning("只有待发送状态的通知才能取消,当前状态:{Status},ID:{HistoryId}", + (NotificationStatusEnum)history.SendStatus, notificationHistoryId); + return false; + } + + history.SendStatus = (int)NotificationStatusEnum.Cancelled; + history.SendResult = "通知已取消"; + history.ActualSendTime = DateTime.Now; + + await _notificationHistoryRepository.UpdateAsync(history); + _logger.LogInformation("成功取消通知,历史记录ID:{HistoryId}", notificationHistoryId); + return true; + } + catch (Exception ex) + { + _logger.LogError(ex, "取消通知异常,历史记录ID:{HistoryId}", notificationHistoryId); + return false; + } + } + + /// + /// 批量取消通知 + /// + [HttpPost] + public async Task BatchCancelNotificationsAsync(List notificationHistoryIds) + { + _logger.LogInformation("开始批量取消通知,数量:{Count}", notificationHistoryIds.Count); + + int successCount = 0; + foreach (var historyId in notificationHistoryIds) + { + try + { + bool success = await CancelNotificationAsync(historyId); + if (success) successCount++; + } + catch (Exception ex) + { + _logger.LogError(ex, "批量取消通知异常,历史记录ID:{HistoryId}", historyId); + } + } + + _logger.LogInformation("完成批量取消通知,总数:{Total},成功:{Success}", + notificationHistoryIds.Count, successCount); + return successCount; + } + + #endregion + + #region 定时任务管理(决策点6) + + /// + /// 创建定时通知任务 + /// + [HttpPost] + public async Task CreateScheduledNotificationTaskAsync( + long notificationSettingId, + string businessType, + long? businessId = null, + string businessData = "", + DateTime? plannedExecutionTime = null, + string cronExpression = "") + { + _logger.LogInformation("开始创建定时通知任务,通知设置ID:{SettingId},业务类型:{BusinessType}", + notificationSettingId, businessType); + + // 简化实现,返回模拟任务ID + var taskId = DateTime.Now.Ticks; + _logger.LogInformation("成功创建定时通知任务,任务ID:{TaskId}", taskId); + return taskId; + } + + /// + /// 执行定时通知任务 + /// + [HttpPost] + public async Task ExecuteScheduledNotificationTaskAsync(long taskId) + { + _logger.LogInformation("开始执行定时通知任务,任务ID:{TaskId}", taskId); + await Task.Delay(100); + _logger.LogInformation("成功执行定时通知任务,任务ID:{TaskId}", taskId); + return true; + } + + /// + /// 获取待执行的定时任务列表 + /// + [HttpGet] + public async Task> GetPendingNotificationTasksAsync() + { + await Task.CompletedTask; + return new List(); + } + + /// + /// 启用/禁用定时任务 + /// + [HttpPut] + public async Task ToggleNotificationTaskAsync(long taskId, bool enabled) + { + _logger.LogInformation("开始{Action}定时任务,任务ID:{TaskId}", enabled ? "启用" : "禁用", taskId); + await Task.CompletedTask; + _logger.LogInformation("成功{Action}定时任务,任务ID:{TaskId}", enabled ? "启用" : "禁用", taskId); + } + + #endregion + + #region 通知统计 + + /// + /// 获取通知发送统计信息 + /// + [HttpPost] + public async Task> GetNotificationStatisticsAsync( + DateTime startTime, + DateTime endTime, + long? notificationSettingId = null) + { + _logger.LogInformation("开始获取通知发送统计信息,时间范围:{StartTime} - {EndTime}", startTime, endTime); + + var statistics = new Dictionary + { + ["TotalCount"] = 0, + ["SuccessCount"] = 0, + ["FailedCount"] = 0, + ["PendingCount"] = 0, + ["SuccessRate"] = 0.0, + ["EmailCount"] = 0, + ["SystemMessageCount"] = 0, + ["StatisticsTime"] = DateTime.Now + }; + + _logger.LogInformation("成功获取通知发送统计信息"); + return statistics; + } + + #endregion + + #region 私有辅助方法 + + /// + /// 用户信息DTO类 + /// + private class UserInfo + { + public long Id { get; set; } + public string Name { get; set; } = ""; + public string Email { get; set; } = ""; + } + + /// + /// 获取用户信息字典 + /// + private async Task> GetUserInfoDictionary(List personnelIds) + { + var userInfoDict = new Dictionary(); + + try + { + var cacheKey = CacheKeys.UserListKey; + var cacheOrgUsers = Cache.Get>(cacheKey); + + + if (cacheOrgUsers == null || (cacheOrgUsers != null && cacheOrgUsers.Count == 0)) + { + cacheOrgUsers = await _userService.GetUserList(); + Cache.Set(cacheKey, cacheOrgUsers, new TimeSpan(8, 0, 0)); + } + + foreach (var personnelId in personnelIds) + { + var cu = cacheOrgUsers.Where(x => x.Id == personnelId).FirstOrDefault(); + userInfoDict[personnelId] = new UserInfo + { + Id = personnelId, + Name = cu?.Name, + Email = cu?.Email + }; + } + } + catch (Exception ex) + { + _logger.LogError(ex, "获取用户信息异常,人员ID列表:{PersonnelIds}", string.Join(",", personnelIds)); + } + + return userInfoDict; + } + + /// + /// 发送单个通知 + /// + private async Task SendSingleNotification(SendNotificationInput input, UserInfo userInfo) + { + try + { + if (input.NotificationType == NotificationTypeEnum.Email) + { + return await SendEmailNotification(userInfo.Email, input.Title, input.Content); + } + else if (input.NotificationType == NotificationTypeEnum.SystemMessage) + { + return true; + } + + return false; + } + catch (Exception ex) + { + _logger.LogError(ex, "发送单个通知异常,人员ID:{PersonnelId},通知方式:{NotificationType}", + userInfo.Id, input.NotificationType); + return false; + } + } + + /// + /// 发送群组邮件通知(一封邮件发送给所有人) + /// + private async Task SendGroupEmailNotification(SendNotificationInput input, + List<(long personnelId, UserInfo userInfo)> validUsers, SendNotificationOutput output) + { + try + { + // 收集所有有效的邮箱地址 + var emailAddresses = validUsers + .Where(u => !string.IsNullOrWhiteSpace(u.userInfo.Email)) + .Select(u => u.userInfo.Email) + .Distinct() + .ToList(); + + if (!emailAddresses.Any()) + { + // 没有有效邮箱,标记所有用户为失败 + foreach (var (personnelId, userInfo) in validUsers) + { + var sendResult = new NotificationSendResult + { + RecipientPersonnelId = personnelId, + RecipientPersonnelName = userInfo.Name, + RecipientEmail = userInfo.Email, + NotificationType = input.NotificationType, + SendStatus = NotificationStatusEnum.Failed, + ErrorMessage = "邮箱地址无效" + }; + output.SendResults.Add(sendResult); + output.FailedCount++; + } + return; + } + + // 创建批量邮件发送请求 + var batchEmailInput = new BatchSendEmailInput + { + Recipients = emailAddresses, + Subject = input.Title, + Content = input.Content, + IsHtml = true + }; + + // 发送批量邮件 + var emailResult = await _emailNotificationService.BatchSendEmailAsync(batchEmailInput); + var sendTime = DateTime.Now; + + // 为每个用户创建通知历史记录和发送结果 + foreach (var (personnelId, userInfo) in validUsers) + { + try + { + // 创建通知历史记录 + var historyId = await CreateNotificationHistory(input.SettingId, input, personnelId, userInfo); + + // 判断该用户的邮件是否发送成功 + var userEmailResult = emailResult.Results.FirstOrDefault(r => r.RecipientEmail == userInfo.Email); + var isSuccess = userEmailResult?.IsSuccess == true; + + var sendResult = new NotificationSendResult + { + RecipientPersonnelId = personnelId, + RecipientPersonnelName = userInfo.Name, + RecipientEmail = userInfo.Email, + NotificationType = input.NotificationType, + NotificationHistoryId = historyId, + SendTime = sendTime + }; + + if (isSuccess) + { + sendResult.SendStatus = NotificationStatusEnum.Success; + sendResult.SendResult = "群组邮件发送成功"; + output.SuccessCount++; + + // 更新历史记录状态 + await UpdateNotificationHistoryStatus(historyId, NotificationStatusEnum.Success, "群组邮件发送成功", null); + } + else + { + sendResult.SendStatus = NotificationStatusEnum.Failed; + sendResult.ErrorMessage = userEmailResult?.ErrorMessage ?? "群组邮件发送失败"; + output.FailedCount++; + + // 更新历史记录状态 + await UpdateNotificationHistoryStatus(historyId, NotificationStatusEnum.Failed, null, sendResult.ErrorMessage); + } + + output.SendResults.Add(sendResult); + output.NotificationHistoryIds.Add(historyId); + } + catch (Exception ex) + { + _logger.LogError(ex, "处理群组邮件发送结果异常,人员ID:{PersonnelId}", personnelId); + + var sendResult = new NotificationSendResult + { + RecipientPersonnelId = personnelId, + RecipientPersonnelName = userInfo.Name, + RecipientEmail = userInfo.Email, + NotificationType = input.NotificationType, + SendStatus = NotificationStatusEnum.Failed, + ErrorMessage = ex.Message + }; + output.SendResults.Add(sendResult); + output.FailedCount++; + } + } + + _logger.LogInformation("群组邮件发送完成,邮箱数量:{EmailCount},用户数量:{UserCount}", + emailAddresses.Count, validUsers.Count); + } + catch (Exception ex) + { + _logger.LogError(ex, "发送群组邮件异常"); + throw; + } + } + + /// + /// 发送个人系统消息(逐个发送) + /// + private async Task SendIndividualSystemMessages(SendNotificationInput input, + List<(long personnelId, UserInfo userInfo)> validUsers, SendNotificationOutput output) + { + try + { + foreach (var (personnelId, userInfo) in validUsers) + { + var sendResult = new NotificationSendResult + { + RecipientPersonnelId = personnelId, + RecipientPersonnelName = userInfo.Name, + RecipientEmail = userInfo.Email, + NotificationType = input.NotificationType + }; + + try + { + // 创建通知历史记录 + var historyId = await CreateNotificationHistory(input.SettingId, input, personnelId, userInfo); + sendResult.NotificationHistoryId = historyId; + + // 发送系统消息 + var success = await SendSystemMessageNotification(personnelId, input.Title, input.Content); + + if (success) + { + sendResult.SendStatus = NotificationStatusEnum.Success; + sendResult.SendResult = "系统消息发送成功"; + sendResult.SendTime = DateTime.Now; + output.SuccessCount++; + + // 更新历史记录状态 + await UpdateNotificationHistoryStatus(historyId, NotificationStatusEnum.Success, "系统消息发送成功", null); + } + else + { + sendResult.SendStatus = NotificationStatusEnum.Failed; + sendResult.ErrorMessage = "系统消息发送失败"; + output.FailedCount++; + + // 更新历史记录状态 + await UpdateNotificationHistoryStatus(historyId, NotificationStatusEnum.Failed, null, "系统消息发送失败"); + } + } + catch (Exception ex) + { + sendResult.SendStatus = NotificationStatusEnum.Failed; + sendResult.ErrorMessage = ex.Message; + output.FailedCount++; + _logger.LogError(ex, "发送系统消息异常,人员ID:{PersonnelId}", personnelId); + + // 更新历史记录状态 + if (sendResult.NotificationHistoryId > 0) + { + await UpdateNotificationHistoryStatus(sendResult.NotificationHistoryId, + NotificationStatusEnum.Failed, null, ex.Message); + } + } + + output.SendResults.Add(sendResult); + output.NotificationHistoryIds.Add(sendResult.NotificationHistoryId); + } + + _logger.LogInformation("个人系统消息发送完成,用户数量:{UserCount}", validUsers.Count); + } + catch (Exception ex) + { + _logger.LogError(ex, "发送个人系统消息异常"); + throw; + } + } + + /// + /// 发送邮件通知 + /// + private async Task SendEmailNotification(string email, string title, string content) + { + try + { + var sendEmailInput = new SendEmailInput + { + RecipientEmail = email, + Subject = title, + Content = content + }; + + var result = await _emailNotificationService.SendEmailAsync(sendEmailInput); + return result.IsSuccess; + } + catch (Exception ex) + { + _logger.LogError(ex, "发送邮件通知异常,邮箱:{Email}", email); + return false; + } + } + + /// + /// 发送系统消息通知 + /// + private async Task SendSystemMessageNotification(long personnelId, string title, string content) + { + try + { + var sendMessageInput = new SendSystemMessageInput + { + RecipientPersonnelId = personnelId, + Title = title, + Content = content + }; + + var result = await _systemMessageService.SendSystemMessageAsync(sendMessageInput); + return result.Success; + } + catch (Exception ex) + { + _logger.LogError(ex, "发送系统消息异常,人员ID:{PersonnelId}", personnelId); + return false; + } + } + + /// + /// 创建通知历史记录 + /// + private async Task CreateNotificationHistory(long? notificationSettingId, SendNotificationInput input, + long personnelId, UserInfo userInfo) + { + try + { + var history = new NotificationHistoryEntity + { + NotificationSettingId = notificationSettingId ?? 0, + RecipientPersonnelId = personnelId, + RecipientPersonnelName = userInfo.Name, + RecipientEmail = userInfo.Email, + NotificationType = (int)input.NotificationType, + NotificationTitle = input.Title, + NotificationContent = input.Content, + PlannedSendTime = input.PlannedSendTime ?? DateTime.Now, + SendStatus = (int)NotificationStatusEnum.Pending, + MaxRetryCount = input.MaxRetryCount, + BusinessType = input.BusinessType, + BusinessId = input.BusinessId, + BusinessData = input.BusinessData + }; + + history = await _notificationHistoryRepository.InsertAsync(history); + return history.Id; + } + catch (Exception ex) + { + _logger.LogError(ex, "创建通知历史记录异常,人员ID:{PersonnelId}", personnelId); + return 0; + } + } + + /// + /// 更新通知历史记录状态 + /// + private async Task UpdateNotificationHistoryStatus(long historyId, NotificationStatusEnum status, + string sendResult, string errorMessage) + { + try + { + if (historyId <= 0) return; + + var history = await _notificationHistoryRepository.GetAsync(historyId); + if (history != null) + { + history.SendStatus = (int)status; + history.SendResult = sendResult ?? ""; + history.ErrorMessage = errorMessage ?? ""; + + if (status == NotificationStatusEnum.Success) + { + history.ActualSendTime = DateTime.Now; + } + + await _notificationHistoryRepository.UpdateAsync(history); + } + } + catch (Exception ex) + { + _logger.LogError(ex, "更新通知历史记录状态异常,历史记录ID:{HistoryId}", historyId); + } + } + + /// + /// 映射到NotificationSettingOutput + /// + private NotificationSettingOutput MapToNotificationSettingOutput(NotificationSettingEntity entity) + { + return new NotificationSettingOutput + { + Id = entity.Id, + NotificationName = entity.NotificationName, + Description = entity.Description, + IsEnabled = entity.IsEnabled, + IsEmailEnabled = entity.IsEmailEnabled, + IsSystemMessageEnabled = entity.IsSystemMessageEnabled, + StartTime = entity.StartTime, + EndTime = entity.EndTime, + FrequencyType = (NotificationFrequencyEnum)entity.FrequencyType, + IntervalMinutes = entity.IntervalMinutes, + PersonnelGroupId = entity.PersonnelGroupId, + PersonnelGroupName = entity.PersonnelGroup?.GroupName ?? "", + EmailSubjectTemplate = entity.EmailSubjectTemplate, + EmailContentTemplate = entity.EmailContentTemplate, + SystemMessageTitleTemplate = entity.SystemMessageTitleTemplate, + SystemMessageContentTemplate = entity.SystemMessageContentTemplate, + TriggerConditions = entity.TriggerConditions, + CreatedTime = entity.CreatedTime ?? DateTime.MinValue, + ModifiedTime = entity.ModifiedTime, + LastModifiedTime = entity.LastModifiedTime + }; + } + + /// + /// 映射到PersonnelGroupOutput + /// + private PersonnelGroupOutput MapToPersonnelGroupOutput(PersonnelGroupEntity entity) + { + var staticPersonnelIds = JsonSerializer.Deserialize>(entity.StaticPersonnelIds) ?? new List(); + var dynamicDepartmentIds = JsonSerializer.Deserialize>(entity.DynamicDepartmentIds) ?? new List(); + var dynamicPositions = JsonSerializer.Deserialize>(entity.DynamicPositions) ?? new List(); + var excludePersonnelIds = JsonSerializer.Deserialize>(entity.ExcludePersonnelIds) ?? new List(); + + return new PersonnelGroupOutput + { + Id = entity.Id, + GroupName = entity.GroupName, + Description = entity.Description, + GroupType = (PersonnelGroupTypeEnum)entity.GroupType, + IsEnabled = entity.IsEnabled, + StaticPersonnelIds = staticPersonnelIds, + DynamicDepartmentIds = dynamicDepartmentIds, + DynamicPositions = dynamicPositions, + OnlyActivePersonnel = entity.OnlyActivePersonnel, + ExcludePersonnelIds = excludePersonnelIds, + CreatedTime = entity.CreatedTime ?? DateTime.MinValue, + ModifiedTime = entity.ModifiedTime, + LastModifiedTime = entity.LastModifiedTime + }; + } + + /// + /// 映射到NotificationHistoryOutput + /// + private NotificationHistoryOutput MapToNotificationHistoryOutput(NotificationHistoryEntity entity) + { + return new NotificationHistoryOutput + { + Id = entity.Id, + NotificationSettingId = entity.NotificationSettingId, + NotificationSettingName = entity.NotificationSetting?.NotificationName ?? "", + RecipientPersonnelId = entity.RecipientPersonnelId, + RecipientPersonnelName = entity.RecipientPersonnelName, + RecipientEmail = entity.RecipientEmail, + NotificationType = (NotificationTypeEnum)entity.NotificationType, + NotificationTitle = entity.NotificationTitle, + NotificationContent = entity.NotificationContent, + PlannedSendTime = entity.PlannedSendTime, + ActualSendTime = entity.ActualSendTime, + SendStatus = (NotificationStatusEnum)entity.SendStatus, + SendResult = entity.SendResult, + ErrorMessage = entity.ErrorMessage, + RetryCount = entity.RetryCount, + MaxRetryCount = entity.MaxRetryCount, + NextRetryTime = entity.NextRetryTime, + BusinessType = entity.BusinessType, + BusinessId = entity.BusinessId, + BusinessData = entity.BusinessData, + CreatedTime = entity.CreatedTime ?? DateTime.MinValue + }; + } + + #endregion + + #region 私有辅助方法验证 + + /// + /// 验证时间配置合法性 + /// + private void ValidateTimeConfiguration(string startTime, string endTime) + { + if (!string.IsNullOrEmpty(startTime) && !string.IsNullOrEmpty(endTime)) + { + if (TimeOnly.TryParse(startTime, out var start) && TimeOnly.TryParse(endTime, out var end)) + { + if (start >= end) + { + throw new Exception("开始时间不能大于等于结束时间"); + } + } + } + } + + /// + /// 验证频次配置合法性 + /// + private void ValidateFrequencyConfiguration(NotificationFrequencyEnum frequencyType, int? intervalMinutes) + { + if (frequencyType == NotificationFrequencyEnum.FixedInterval && (!intervalMinutes.HasValue || intervalMinutes.Value <= 0)) + { + throw new Exception("间隔频次时,间隔分钟数必须大于0"); + } + } + + /// + /// 验证模板配置合法性 + /// + private async Task ValidateTemplateConfiguration(dynamic input) + { + if (!string.IsNullOrEmpty(input.EmailSubjectTemplate)) + { + var result = await _templateService.ValidateTemplateAsync(new ValidateTemplateInput { Template = input.EmailSubjectTemplate }); + if (!result.Success) + { + throw new Exception($"邮件主题模板格式错误:{result.ErrorMessage}"); + } + } + } + + /// + /// 验证人员组配置合法性 + /// + private void ValidatePersonnelGroupConfiguration(PersonnelGroupCreateInput input) + { + var groupType = (PersonnelGroupTypeEnum)input.GroupType; + + if (groupType == PersonnelGroupTypeEnum.Static && (input.StaticPersonnelIds == null || !input.StaticPersonnelIds.Any())) + { + throw new Exception("静态人员组必须配置人员"); + } + + if ((groupType == PersonnelGroupTypeEnum.DynamicByDepartment || groupType == PersonnelGroupTypeEnum.Mixed) && + (input.DynamicDepartmentIds == null || !input.DynamicDepartmentIds.Any())) + { + throw new Exception("动态部门人员组必须配置部门"); + } + } + + /// + /// 获取动态人员ID列表 + /// + private async Task> GetDynamicPersonnelIds(PersonnelGroupEntity personnelGroup) + { + var orgUserIds = new List(); + + var dynamicDepartmentIds = JsonSerializer.Deserialize>(personnelGroup.DynamicDepartmentIds); + foreach (var orgId in dynamicDepartmentIds) + { + var cacheKey = CacheKeys.GetDynamicDepartmentKey(orgId); + + var cacheOrgUserIds = Cache.Get>(cacheKey); + if (cacheOrgUserIds != null && cacheOrgUserIds.Any()) + { + orgUserIds.AddRange(cacheOrgUserIds.Select(m => m.Id).ToList()); + } + else + { + var orgUsers = await _userService.GetOrgUserList(orgId); + Cache.Set(cacheKey, orgUsers, new TimeSpan(8, 0, 0)); + orgUserIds.AddRange(orgUsers.Select(m => m.Id).ToList()); + } + } + return orgUserIds; + } + + #endregion +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Api/Services/Notification/NotificationTemplateService.cs b/NPP.SmartSchedue.Api/Services/Notification/NotificationTemplateService.cs index 1b13f48..806f365 100644 --- a/NPP.SmartSchedue.Api/Services/Notification/NotificationTemplateService.cs +++ b/NPP.SmartSchedue.Api/Services/Notification/NotificationTemplateService.cs @@ -3,17 +3,22 @@ using System.Collections.Generic; using System.Threading.Tasks; using System.Text.RegularExpressions; using System.Linq; +using Microsoft.AspNetCore.Mvc; using Newtonsoft.Json; +using ZhonTai.Admin.Services; +using ZhonTai.DynamicApi; +using ZhonTai.DynamicApi.Attributes; using NPP.SmartSchedue.Api.Contracts.Services.Notification; +using NPP.SmartSchedue.Api.Contracts.Services.Notification.Input; +using NPP.SmartSchedue.Api.Contracts.Services.Notification.Output; namespace NPP.SmartSchedue.Api.Services.Notification; /// -/// 通知模板服务实现 -/// 决策点7:模板通知,支持通知内容模板,可替换变量 -/// 使用简单的变量替换机制:{变量名} +/// 通知服务 模板 /// -public class NotificationTemplateService : INotificationTemplateService +[DynamicApi(Area = "app")] +public class NotificationTemplateService : BaseService, INotificationTemplateService, IDynamicApi { // 变量匹配正则表达式:匹配 {变量名} 格式 private static readonly Regex VariableRegex = new Regex(@"\{([a-zA-Z_][a-zA-Z0-9_]*)\}", RegexOptions.Compiled); @@ -22,16 +27,237 @@ public class NotificationTemplateService : INotificationTemplateService /// /// 渲染通知模板 + /// 支持变量替换,如:{变量名} /// - public async Task RenderTemplateAsync(string template, Dictionary variables) + /// 渲染模板输入参数 + /// 渲染结果 + [HttpPost] + public async Task RenderTemplateAsync(RenderTemplateInput input) { - return await Task.FromResult(RenderTemplate(template, variables)); + try + { + var renderedContent = RenderTemplateCore(input.Template, input.Variables); + + return await Task.FromResult(new RenderTemplateOutput + { + RenderedContent = renderedContent, + Success = true + }); + } + catch (Exception ex) + { + return new RenderTemplateOutput + { + Success = false, + ErrorMessage = $"渲染模板时发生错误:{ex.Message}" + }; + } + } + + #endregion + + #region 模板验证 + + /// + /// 验证模板语法是否正确 + /// + /// 验证模板输入参数 + /// 验证结果 + [HttpPost] + public async Task ValidateTemplateAsync(ValidateTemplateInput input) + { + try + { + var validationResult = ValidateTemplateCore(input.Template); + + return await Task.FromResult(new ValidateTemplateOutput + { + ValidationResult = validationResult, + Success = true + }); + } + catch (Exception ex) + { + return new ValidateTemplateOutput + { + Success = false, + ErrorMessage = $"验证模板时发生错误:{ex.Message}", + ValidationResult = new TemplateValidationResult() + }; + } + } + + #endregion + + #region 变量解析 + + /// + /// 提取模板中的变量列表 + /// + /// 提取变量输入参数 + /// 变量列表 + [HttpPost] + public async Task ExtractVariablesAsync(ExtractVariablesInput input) + { + try + { + var variables = ExtractVariablesCore(input.Template); + + return await Task.FromResult(new ExtractVariablesOutput + { + Variables = variables, + Success = true + }); + } + catch (Exception ex) + { + return new ExtractVariablesOutput + { + Success = false, + ErrorMessage = $"提取变量时发生错误:{ex.Message}" + }; + } + } + + #endregion + + #region 内置变量 + + /// + /// 获取系统内置变量 + /// + /// 系统变量字典 + [HttpPost] + public async Task GetSystemVariablesAsync() + { + try + { + var systemVariables = GetSystemVariablesCore(); + + return await Task.FromResult(new GetSystemVariablesOutput + { + Variables = systemVariables, + Success = true + }); + } + catch (Exception ex) + { + return new GetSystemVariablesOutput + { + Success = false, + ErrorMessage = $"获取系统变量时发生错误:{ex.Message}" + }; + } } /// - /// 同步渲染通知模板 + /// 获取业务相关变量 /// - public string RenderTemplate(string template, Dictionary variables) + /// 获取业务变量输入参数 + /// 业务变量字典 + [HttpPost] + public async Task GetBusinessVariablesAsync(GetBusinessVariablesInput input) + { + try + { + var businessVariables = await GetBusinessVariablesCore(input.BusinessType, input.BusinessId, input.BusinessData); + + return new GetBusinessVariablesOutput + { + Variables = businessVariables, + Success = true + }; + } + catch (Exception ex) + { + return new GetBusinessVariablesOutput + { + Success = false, + ErrorMessage = $"获取业务变量时发生错误:{ex.Message}" + }; + } + } + + #endregion + + #region 模板预定义 + + /// + /// 获取预定义模板列表 + /// + /// 获取模板列表输入参数 + /// 预定义模板列表 + [HttpPost] + public async Task GetPredefinedTemplatesAsync(GetPredefinedTemplatesInput input) + { + try + { + var templates = GetDefaultPredefinedTemplates(); + + if (!string.IsNullOrWhiteSpace(input.Category)) + { + templates = templates.Where(t => + string.Equals(t.Category, input.Category, StringComparison.OrdinalIgnoreCase)).ToList(); + } + + return await Task.FromResult(new GetPredefinedTemplatesOutput + { + Templates = templates, + TotalCount = templates.Count, + Success = true + }); + } + catch (Exception ex) + { + return new GetPredefinedTemplatesOutput + { + Success = false, + ErrorMessage = $"获取预定义模板列表时发生错误:{ex.Message}" + }; + } + } + + /// + /// 获取指定预定义模板 + /// + /// 获取指定模板输入参数 + /// 预定义模板 + [HttpPost] + public async Task GetPredefinedTemplateAsync(GetPredefinedTemplateInput input) + { + try + { + var templates = GetDefaultPredefinedTemplates(); + var template = templates.FirstOrDefault(t => + string.Equals(t.TemplateId, input.TemplateId, StringComparison.OrdinalIgnoreCase)); + + return await Task.FromResult(new GetPredefinedTemplateOutput + { + Template = template, + Success = true + }); + } + catch (Exception ex) + { + return new GetPredefinedTemplateOutput + { + Success = false, + ErrorMessage = $"获取指定预定义模板时发生错误:{ex.Message}" + }; + } + } + + #endregion + + #region 核心业务逻辑方法 + + /// + /// 核心模板渲染逻辑 + /// + /// 模板内容 + /// 变量字典 + /// 渲染后的内容 + private string RenderTemplateCore(string template, Dictionary variables) { if (string.IsNullOrWhiteSpace(template)) return string.Empty; @@ -59,22 +285,12 @@ public class NotificationTemplateService : INotificationTemplateService return result; } - #endregion - - #region 模板验证 - /// - /// 验证模板语法 + /// 核心模板验证逻辑 /// - public async Task ValidateTemplateAsync(string template) - { - return await Task.FromResult(ValidateTemplate(template)); - } - - /// - /// 同步验证模板语法 - /// - public TemplateValidationResult ValidateTemplate(string template) + /// 模板内容 + /// 验证结果 + private TemplateValidationResult ValidateTemplateCore(string template) { var result = new TemplateValidationResult(); @@ -87,7 +303,7 @@ public class NotificationTemplateService : INotificationTemplateService try { // 提取所有变量 - var variables = ExtractVariables(template); + var variables = ExtractVariablesCore(template); result.Variables = variables; // 检查变量命名规范 @@ -127,22 +343,12 @@ public class NotificationTemplateService : INotificationTemplateService return result; } - #endregion - - #region 变量解析 - /// - /// 提取模板中的变量列表 + /// 核心变量提取逻辑 /// - public async Task> ExtractVariablesAsync(string template) - { - return await Task.FromResult(ExtractVariables(template)); - } - - /// - /// 同步提取模板中的变量列表 - /// - public List ExtractVariables(string template) + /// 模板内容 + /// 变量名列表 + private List ExtractVariablesCore(string template) { if (string.IsNullOrWhiteSpace(template)) return new List(); @@ -159,18 +365,15 @@ public class NotificationTemplateService : INotificationTemplateService return variables.OrderBy(v => v).ToList(); } - #endregion - - #region 内置变量 - /// - /// 获取系统内置变量 + /// 获取系统内置变量核心逻辑 /// - public async Task> GetSystemVariablesAsync() + /// 系统变量字典 + private Dictionary GetSystemVariablesCore() { var now = DateTime.Now; - var systemVariables = new Dictionary + return new Dictionary { ["CurrentDateTime"] = now.ToString("yyyy-MM-dd HH:mm:ss"), ["CurrentDate"] = now.ToString("yyyy-MM-dd"), @@ -182,17 +385,19 @@ public class NotificationTemplateService : INotificationTemplateService ["SystemName"] = "NPP智能生产调度系统", ["CompanyName"] = "核电站" }; - - return await Task.FromResult(systemVariables); } /// - /// 获取业务相关变量 + /// 获取业务相关变量核心逻辑 /// - public async Task> GetBusinessVariablesAsync( - string businessType, - long? businessId = null, - string businessData = "") + /// 业务类型 + /// 业务ID + /// 业务数据 + /// 业务变量字典 + private async Task> GetBusinessVariablesCore( + string businessType, + long? businessId, + string businessData) { var businessVariables = new Dictionary { @@ -226,43 +431,13 @@ public class NotificationTemplateService : INotificationTemplateService #endregion - #region 模板预定义 - - /// - /// 获取预定义模板列表 - /// - public async Task> GetPredefinedTemplatesAsync(string category = "") - { - var templates = GetDefaultPredefinedTemplates(); - - if (!string.IsNullOrWhiteSpace(category)) - { - templates = templates.Where(t => - string.Equals(t.Category, category, StringComparison.OrdinalIgnoreCase)).ToList(); - } - - return await Task.FromResult(templates); - } - - /// - /// 获取指定预定义模板 - /// - public async Task GetPredefinedTemplateAsync(string templateId) - { - var templates = GetDefaultPredefinedTemplates(); - var template = templates.FirstOrDefault(t => - string.Equals(t.TemplateId, templateId, StringComparison.OrdinalIgnoreCase)); - - return await Task.FromResult(template); - } - - #endregion - #region 私有辅助方法 /// /// 验证变量名是否符合规范 /// + /// 变量名 + /// 是否符合规范 private static bool IsValidVariableName(string variableName) { if (string.IsNullOrWhiteSpace(variableName)) @@ -275,6 +450,8 @@ public class NotificationTemplateService : INotificationTemplateService /// /// 获取星期几的中文名称 /// + /// 星期枚举 + /// 中文名称 private static string GetWeekdayName(DayOfWeek dayOfWeek) { return dayOfWeek switch @@ -293,6 +470,10 @@ public class NotificationTemplateService : INotificationTemplateService /// /// 添加工作任务相关变量 /// + /// 变量字典 + /// 业务ID + /// 业务数据 + /// 异步任务 private async Task AddWorkOrderVariablesAsync(Dictionary variables, long? businessId, string businessData) { // 这里可以根据businessId查询工作任务详情,然后添加相关变量 @@ -324,6 +505,10 @@ public class NotificationTemplateService : INotificationTemplateService /// /// 添加设备相关变量 /// + /// 变量字典 + /// 业务ID + /// 业务数据 + /// 异步任务 private async Task AddEquipmentVariablesAsync(Dictionary variables, long? businessId, string businessData) { variables["EquipmentId"] = businessId?.ToString() ?? ""; @@ -334,6 +519,10 @@ public class NotificationTemplateService : INotificationTemplateService /// /// 添加人员相关变量 /// + /// 变量字典 + /// 业务ID + /// 业务数据 + /// 异步任务 private async Task AddPersonnelVariablesAsync(Dictionary variables, long? businessId, string businessData) { variables["PersonnelId"] = businessId?.ToString() ?? ""; @@ -344,6 +533,10 @@ public class NotificationTemplateService : INotificationTemplateService /// /// 添加维护相关变量 /// + /// 变量字典 + /// 业务ID + /// 业务数据 + /// 异步任务 private async Task AddMaintenanceVariablesAsync(Dictionary variables, long? businessId, string businessData) { variables["MaintenanceId"] = businessId?.ToString() ?? ""; @@ -354,6 +547,7 @@ public class NotificationTemplateService : INotificationTemplateService /// /// 获取默认预定义模板 /// + /// 预定义模板列表 private static List GetDefaultPredefinedTemplates() { return new List diff --git a/NPP.SmartSchedue.Api/Services/Notification/SystemMessageService.cs b/NPP.SmartSchedue.Api/Services/Notification/SystemMessageService.cs index 7ed812d..930207e 100644 --- a/NPP.SmartSchedue.Api/Services/Notification/SystemMessageService.cs +++ b/NPP.SmartSchedue.Api/Services/Notification/SystemMessageService.cs @@ -2,43 +2,35 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; using System.Linq; +using Microsoft.AspNetCore.Mvc; using Microsoft.Extensions.Logging; -using FreeSql; -using ZhonTai.Admin.Core.Db.Transaction; +using ZhonTai.Admin.Services; +using ZhonTai.DynamicApi; +using ZhonTai.DynamicApi.Attributes; using NPP.SmartSchedue.Api.Contracts.Services.Notification; -using NPP.SmartSchedue.Api.Contracts.Core.Repositories; +using NPP.SmartSchedue.Api.Contracts.Services.Notification.Input; +using NPP.SmartSchedue.Api.Contracts.Services.Notification.Output; +using ZhonTai.Admin.Core.Repositories; using NPP.SmartSchedue.Api.Contracts.Domain.Notification; +using NPP.SmartSchedue.Api.Contracts.Core.Enums; namespace NPP.SmartSchedue.Api.Services.Notification; /// -/// 系统消息服务实现 -/// 决策点1:基础通知方式 - 系统消息通知 -/// 业务思考:系统消息是内部通知的核心方式,需要支持实时推送、状态管理、操作交互等功能 -/// 设计原则: -/// 1. 消息持久化存储,确保不丢失 -/// 2. 支持消息状态管理(未读/已读/已删除) -/// 3. 支持操作按钮,实现交互式消息 -/// 4. 支持批量操作,提高效率 -/// 5. 支持模板化消息,统一格式 +/// 通知服务 系统消息 /// -public class SystemMessageService : ISystemMessageService +[DynamicApi(Area = "app")] +public class SystemMessageService : BaseService, ISystemMessageService, IDynamicApi { - private readonly ILogger _logger; private readonly INotificationTemplateService _templateService; - private readonly IBaseRepository _notificationHistoryRepository; - private readonly IUnitOfWorkManager _uowManager; + private readonly IRepositoryBase _notificationHistoryRepository; public SystemMessageService( - ILogger logger, INotificationTemplateService templateService, - IBaseRepository notificationHistoryRepository, - IUnitOfWorkManager uowManager) + IRepositoryBase notificationHistoryRepository) { - _logger = logger ?? throw new ArgumentNullException(nameof(logger)); - _templateService = templateService ?? throw new ArgumentNullException(nameof(templateService)); - _notificationHistoryRepository = notificationHistoryRepository ?? throw new ArgumentNullException(nameof(notificationHistoryRepository)); - _uowManager = uowManager ?? throw new ArgumentNullException(nameof(uowManager)); + _templateService = templateService; + _notificationHistoryRepository = notificationHistoryRepository; } #region 单个消息发送 @@ -47,58 +39,47 @@ public class SystemMessageService : ISystemMessageService /// 发送系统消息 /// 业务场景:工作任务分配、设备状态变更、排班通知等 /// - public async Task SendSystemMessageAsync( - long recipientPersonnelId, - string title, - string content, - SystemMessageTypeEnum messageType = SystemMessageTypeEnum.Info, - string businessType = "", - long? businessId = null) + [HttpPost] + public async Task SendSystemMessageAsync(SendSystemMessageInput input) { try { - if (recipientPersonnelId <= 0) + if (input.RecipientPersonnelId <= 0) { - _logger.LogWarning("收件人ID无效:{RecipientId}", recipientPersonnelId); - return false; + return SendSystemMessageOutput.CreateFailure(input.RecipientPersonnelId, "收件人ID无效"); } - if (string.IsNullOrWhiteSpace(title) || string.IsNullOrWhiteSpace(content)) + if (string.IsNullOrWhiteSpace(input.Title) || string.IsNullOrWhiteSpace(input.Content)) { - _logger.LogWarning("消息标题或内容不能为空"); - return false; + return SendSystemMessageOutput.CreateFailure(input.RecipientPersonnelId, "消息标题或内容不能为空"); } - using var uow = _uowManager.Begin(); + var currentTime = DateTime.Now; // 创建通知历史记录 var notificationHistory = new NotificationHistoryEntity { - NotificationType = Core.Enums.NotificationTypeEnum.SystemMessage, - RecipientType = "Personnel", - RecipientId = recipientPersonnelId.ToString(), - Subject = title, - Content = content, - BusinessType = businessType, - BusinessId = businessId, - Status = Core.Enums.NotificationStatusEnum.Sent, - SentTime = DateTime.Now, - MessageType = GetNotificationMessageType(messageType), - IsRead = false, - IsDeleted = false, - CreatedTime = DateTime.Now + NotificationSettingId = input.NotificationSettingId, // 临时设置,实际应该从配置获取 + NotificationType = (int)NotificationTypeEnum.SystemMessage, + RecipientPersonnelId = input.RecipientPersonnelId, + NotificationTitle = input.Title, + NotificationContent = input.Content, + BusinessType = input.BusinessType, + BusinessId = input.BusinessId, + SendStatus = (int)NotificationStatusEnum.Success, + ActualSendTime = currentTime, + PlannedSendTime = currentTime, + CreatedTime = currentTime }; - await _notificationHistoryRepository.InsertAsync(notificationHistory); - await uow.CommitAsync(); - - _logger.LogInformation("系统消息发送成功:收件人 {RecipientId},标题:{Title}", recipientPersonnelId, title); - return true; + var result = await _notificationHistoryRepository.InsertAsync(notificationHistory); + + return SendSystemMessageOutput.CreateSuccess((long)result.Id, input.RecipientPersonnelId, currentTime); } catch (Exception ex) { - _logger.LogError(ex, "发送系统消息失败:收件人 {RecipientId},标题:{Title}", recipientPersonnelId, title); - return false; + Logger.LogError(ex, "发送系统消息失败:收件人 {RecipientId},标题:{Title}", input.RecipientPersonnelId, input.Title); + return SendSystemMessageOutput.CreateFailure(input.RecipientPersonnelId, $"发送失败:{ex.Message}"); } } @@ -106,66 +87,53 @@ public class SystemMessageService : ISystemMessageService /// 发送带操作按钮的系统消息 /// 业务场景:任务确认、审批流程、操作确认等交互式场景 /// - public async Task SendSystemMessageWithActionsAsync( - long recipientPersonnelId, - string title, - string content, - List actions, - SystemMessageTypeEnum messageType = SystemMessageTypeEnum.Info, - string businessType = "", - long? businessId = null) + [HttpPost] + public async Task SendSystemMessageWithActionsAsync(SendSystemMessageWithActionsInput input) { try { - if (recipientPersonnelId <= 0) + if (input.RecipientPersonnelId <= 0) { - _logger.LogWarning("收件人ID无效:{RecipientId}", recipientPersonnelId); - return false; + return SendSystemMessageOutput.CreateFailure(input.RecipientPersonnelId, "收件人ID无效"); } - if (string.IsNullOrWhiteSpace(title) || string.IsNullOrWhiteSpace(content)) + if (string.IsNullOrWhiteSpace(input.Title) || string.IsNullOrWhiteSpace(input.Content)) { - _logger.LogWarning("消息标题或内容不能为空"); - return false; + return SendSystemMessageOutput.CreateFailure(input.RecipientPersonnelId, "消息标题或内容不能为空"); } - using var uow = _uowManager.Begin(); + var currentTime = DateTime.Now; // 序列化操作按钮 - var actionsJson = actions != null && actions.Any() - ? System.Text.Json.JsonSerializer.Serialize(actions) + var actionsJson = input.Actions != null && input.Actions.Any() + ? System.Text.Json.JsonSerializer.Serialize(input.Actions) : ""; // 创建通知历史记录 var notificationHistory = new NotificationHistoryEntity { - NotificationType = Core.Enums.NotificationTypeEnum.SystemMessage, - RecipientType = "Personnel", - RecipientId = recipientPersonnelId.ToString(), - Subject = title, - Content = content, - BusinessType = businessType, - BusinessId = businessId, - Status = Core.Enums.NotificationStatusEnum.Sent, - SentTime = DateTime.Now, - MessageType = GetNotificationMessageType(messageType), - IsRead = false, - IsDeleted = false, - ActionsData = actionsJson, // 存储操作按钮数据 - CreatedTime = DateTime.Now + NotificationSettingId = 1, // 临时设置,实际应该从配置获取 + NotificationType = (int)NotificationTypeEnum.SystemMessage, + RecipientPersonnelId = input.RecipientPersonnelId, + NotificationTitle = input.Title, + NotificationContent = input.Content, + BusinessType = input.BusinessType, + BusinessId = input.BusinessId, + SendStatus = (int)NotificationStatusEnum.Success, + ActualSendTime = currentTime, + PlannedSendTime = currentTime, + BusinessData = actionsJson, // 存储操作按钮数据 + CreatedTime = currentTime }; - await _notificationHistoryRepository.InsertAsync(notificationHistory); - await uow.CommitAsync(); - - _logger.LogInformation("带操作按钮的系统消息发送成功:收件人 {RecipientId},标题:{Title},操作数量:{ActionCount}", - recipientPersonnelId, title, actions?.Count ?? 0); - return true; + var result = await _notificationHistoryRepository.InsertAsync(notificationHistory); + + return SendSystemMessageOutput.CreateSuccess((long)result.Id, input.RecipientPersonnelId, currentTime); } catch (Exception ex) { - _logger.LogError(ex, "发送带操作按钮的系统消息失败:收件人 {RecipientId},标题:{Title}", recipientPersonnelId, title); - return false; + Logger.LogError(ex, "发送带操作按钮的系统消息失败:收件人 {RecipientId},标题:{Title}", input.RecipientPersonnelId, input.Title); + return SendSystemMessageOutput.CreateFailure(input.RecipientPersonnelId, $"发送失败:{ex.Message}"); } } @@ -177,41 +145,33 @@ public class SystemMessageService : ISystemMessageService /// 批量发送系统消息 /// 业务场景:排班变更通知、紧急通知、系统维护通知等需要群发的场景 /// - public async Task> BatchSendSystemMessageAsync( - List recipientPersonnelIds, - string title, - string content, - SystemMessageTypeEnum messageType = SystemMessageTypeEnum.Info, - string businessType = "", - long? businessId = null) + [HttpPost] + public async Task BatchSendSystemMessageAsync(BatchSendSystemMessageInput input) { - var results = new Dictionary(); + var results = new Dictionary(); - if (recipientPersonnelIds == null || !recipientPersonnelIds.Any()) + if (input.RecipientPersonnelIds == null || !input.RecipientPersonnelIds.Any()) { - _logger.LogWarning("批量发送系统消息:收件人列表为空"); - return results; + Logger.LogWarning("批量发送系统消息:收件人列表为空"); + return BatchSendSystemMessageOutput.Create(results); } - if (string.IsNullOrWhiteSpace(title) || string.IsNullOrWhiteSpace(content)) + if (string.IsNullOrWhiteSpace(input.Title) || string.IsNullOrWhiteSpace(input.Content)) { - _logger.LogWarning("消息标题或内容不能为空"); - foreach (var recipientId in recipientPersonnelIds) + foreach (var recipientId in input.RecipientPersonnelIds) { - results[recipientId] = false; + results[recipientId] = SendSystemMessageOutput.CreateFailure(recipientId, "消息标题或内容不能为空"); } - return results; + return BatchSendSystemMessageOutput.Create(results); } // 去重收件人 - var uniqueRecipients = recipientPersonnelIds.Distinct().Where(id => id > 0).ToList(); + var uniqueRecipients = input.RecipientPersonnelIds.Distinct().Where(id => id > 0).ToList(); try { - using var uow = _uowManager.Begin(); - var notificationHistories = new List(); - var messageTypeValue = GetNotificationMessageType(messageType); + // 注释:系统消息类型统一使用NotificationTypeEnum.SystemMessage var currentTime = DateTime.Now; // 批量创建通知记录 @@ -219,66 +179,69 @@ public class SystemMessageService : ISystemMessageService { var notificationHistory = new NotificationHistoryEntity { - NotificationType = Core.Enums.NotificationTypeEnum.SystemMessage, - RecipientType = "Personnel", - RecipientId = recipientId.ToString(), - Subject = title, - Content = content, - BusinessType = businessType, - BusinessId = businessId, - Status = Core.Enums.NotificationStatusEnum.Sent, - SentTime = currentTime, - MessageType = messageTypeValue, - IsRead = false, - IsDeleted = false, + NotificationSettingId = 1, // 临时设置,实际应该从配置获取 + NotificationType = (int)NotificationTypeEnum.SystemMessage, + RecipientPersonnelId = recipientId, + NotificationTitle = input.Title, + NotificationContent = input.Content, + BusinessType = input.BusinessType, + BusinessId = input.BusinessId, + SendStatus = (int)NotificationStatusEnum.Success, + ActualSendTime = currentTime, + PlannedSendTime = currentTime, CreatedTime = currentTime }; notificationHistories.Add(notificationHistory); - results[recipientId] = true; // 预设为成功,如果出错会被重置 } // 批量插入 - await _notificationHistoryRepository.InsertAsync(notificationHistories); - await uow.CommitAsync(); + var insertedEntities = await _notificationHistoryRepository.InsertAsync(notificationHistories); + + // 创建成功结果 + for (int i = 0; i < uniqueRecipients.Count; i++) + { + var recipientId = uniqueRecipients[i]; + var entity = insertedEntities[i]; + results[recipientId] = SendSystemMessageOutput.CreateSuccess((long)entity.Id, recipientId, currentTime); + } - var successCount = results.Values.Count(r => r); - _logger.LogInformation("批量发送系统消息完成:总数 {Total},成功 {Success},失败 {Failed}", - recipientPersonnelIds.Count, successCount, recipientPersonnelIds.Count - successCount); + Logger.LogInformation("批量发送系统消息完成:总数 {Total},成功 {Success}", + input.RecipientPersonnelIds.Count, results.Values.Count(r => r.Success)); } catch (Exception ex) { - _logger.LogError(ex, "批量发送系统消息失败"); + Logger.LogError(ex, "批量发送系统消息失败"); // 如果批量操作失败,将所有结果设为失败 foreach (var recipientId in uniqueRecipients) { - results[recipientId] = false; + results[recipientId] = SendSystemMessageOutput.CreateFailure(recipientId, $"批量发送失败:{ex.Message}"); } } - return results; + return BatchSendSystemMessageOutput.Create(results); } /// /// 个性化批量发送系统消息 /// 业务场景:个性化任务分配通知、个性化提醒等需要不同内容的场景 /// - public async Task> BatchSendPersonalizedSystemMessageAsync(List messageItems) + [HttpPost] + public async Task BatchSendPersonalizedSystemMessageAsync(List messageItems) { - var results = new Dictionary(); + var results = new Dictionary(); if (messageItems == null || !messageItems.Any()) { - _logger.LogWarning("个性化批量发送系统消息:消息项列表为空"); - return results; + Logger.LogWarning("个性化批量发送系统消息:消息项列表为空"); + return BatchSendSystemMessageOutput.Create(results); } try { - using var uow = _uowManager.Begin(); - var notificationHistories = new List(); var currentTime = DateTime.Now; + var successfulItems = new List(); foreach (var item in messageItems) { @@ -288,9 +251,8 @@ public class SystemMessageService : ISystemMessageService string.IsNullOrWhiteSpace(item.Title) || string.IsNullOrWhiteSpace(item.Content)) { - _logger.LogWarning("消息项无效:收件人ID {RecipientId},标题:{Title}", - item.RecipientPersonnelId, item.Title); - results[item.RecipientPersonnelId] = false; + results[item.RecipientPersonnelId] = SendSystemMessageOutput.CreateFailure( + item.RecipientPersonnelId, "消息项无效:收件人ID、标题或内容为空"); continue; } @@ -300,8 +262,18 @@ public class SystemMessageService : ISystemMessageService if (item.Variables != null && item.Variables.Any()) { - title = await _templateService.RenderTemplateAsync(item.Title, item.Variables); - content = await _templateService.RenderTemplateAsync(item.Content, item.Variables); + var titleResult = await _templateService.RenderTemplateAsync(new RenderTemplateInput + { + Template = item.Title, + Variables = item.Variables + }); + var contentResult = await _templateService.RenderTemplateAsync(new RenderTemplateInput + { + Template = item.Content, + Variables = item.Variables + }); + title = titleResult.RenderedContent; + content = contentResult.RenderedContent; } // 序列化操作按钮 @@ -311,54 +283,64 @@ public class SystemMessageService : ISystemMessageService var notificationHistory = new NotificationHistoryEntity { - NotificationType = Core.Enums.NotificationTypeEnum.SystemMessage, - RecipientType = "Personnel", - RecipientId = item.RecipientPersonnelId.ToString(), - Subject = title, - Content = content, + NotificationSettingId = 1, // 临时设置,实际应该从配置获取 + NotificationType = (int)NotificationTypeEnum.SystemMessage, + RecipientPersonnelId = item.RecipientPersonnelId, + NotificationTitle = title, + NotificationContent = content, BusinessType = item.BusinessType, BusinessId = item.BusinessId, - Status = Core.Enums.NotificationStatusEnum.Sent, - SentTime = currentTime, - MessageType = GetNotificationMessageType(item.MessageType), - IsRead = false, - IsDeleted = false, - ActionsData = actionsJson, + SendStatus = (int)NotificationStatusEnum.Success, + ActualSendTime = currentTime, + PlannedSendTime = currentTime, + BusinessData = actionsJson, CreatedTime = currentTime }; notificationHistories.Add(notificationHistory); - results[item.RecipientPersonnelId] = true; + successfulItems.Add(item); } catch (Exception ex) { - _logger.LogError(ex, "处理个性化消息项失败:收件人 {RecipientId}", item.RecipientPersonnelId); - results[item.RecipientPersonnelId] = false; + Logger.LogError(ex, "处理个性化消息项失败:收件人 {RecipientId}", item.RecipientPersonnelId); + results[item.RecipientPersonnelId] = SendSystemMessageOutput.CreateFailure( + item.RecipientPersonnelId, $"处理失败:{ex.Message}"); } } // 批量插入成功的记录 if (notificationHistories.Any()) { - await _notificationHistoryRepository.InsertAsync(notificationHistories); - await uow.CommitAsync(); + var insertedEntities = await _notificationHistoryRepository.InsertAsync(notificationHistories); + + // 创建成功结果 + for (int i = 0; i < successfulItems.Count; i++) + { + var item = successfulItems[i]; + var entity = insertedEntities[i]; + results[item.RecipientPersonnelId] = SendSystemMessageOutput.CreateSuccess( + (long)entity.Id, item.RecipientPersonnelId, currentTime); + } } - var successCount = results.Values.Count(r => r); - _logger.LogInformation("个性化批量发送系统消息完成:总数 {Total},成功 {Success},失败 {Failed}", - messageItems.Count, successCount, messageItems.Count - successCount); + Logger.LogInformation("个性化批量发送系统消息完成:总数 {Total},成功 {Success},失败 {Failed}", + messageItems.Count, results.Values.Count(r => r.Success), results.Values.Count(r => !r.Success)); } catch (Exception ex) { - _logger.LogError(ex, "个性化批量发送系统消息失败"); + Logger.LogError(ex, "个性化批量发送系统消息失败"); // 如果批量操作失败,将所有结果设为失败 foreach (var item in messageItems) { - results[item.RecipientPersonnelId] = false; + if (!results.ContainsKey(item.RecipientPersonnelId)) + { + results[item.RecipientPersonnelId] = SendSystemMessageOutput.CreateFailure( + item.RecipientPersonnelId, $"批量操作失败:{ex.Message}"); + } } } - return results; + return BatchSendSystemMessageOutput.Create(results); } #endregion @@ -369,28 +351,44 @@ public class SystemMessageService : ISystemMessageService /// 使用模板发送系统消息 /// 业务场景:标准化通知,如任务分配模板、维护提醒模板等 /// - public async Task SendSystemMessageByTemplateAsync( - long recipientPersonnelId, - string titleTemplate, - string contentTemplate, - Dictionary variables, - SystemMessageTypeEnum messageType = SystemMessageTypeEnum.Info, - string businessType = "", - long? businessId = null) + [HttpPost] + public async Task SendSystemMessageByTemplateAsync(SendSystemMessageByTemplateInput input) { try { // 渲染模板 - var title = await _templateService.RenderTemplateAsync(titleTemplate, variables); - var content = await _templateService.RenderTemplateAsync(contentTemplate, variables); + var titleResult = await _templateService.RenderTemplateAsync(new RenderTemplateInput + { + Template = input.TitleTemplate, + Variables = input.Variables + }); + var contentResult = await _templateService.RenderTemplateAsync(new RenderTemplateInput + { + Template = input.ContentTemplate, + Variables = input.Variables + }); + + var title = titleResult.RenderedContent; + var content = contentResult.RenderedContent; + + // 创建发送消息的输入模型 + var sendInput = new SendSystemMessageInput + { + RecipientPersonnelId = input.RecipientPersonnelId, + Title = title, + Content = content, + MessageType = input.MessageType, + BusinessType = input.BusinessType, + BusinessId = input.BusinessId + }; // 发送消息 - return await SendSystemMessageAsync(recipientPersonnelId, title, content, messageType, businessType, businessId); + return await SendSystemMessageAsync(sendInput); } catch (Exception ex) { - _logger.LogError(ex, "使用模板发送系统消息失败:收件人 {RecipientId}", recipientPersonnelId); - return false; + Logger.LogError(ex, "使用模板发送系统消息失败:收件人 {RecipientId}", input.RecipientPersonnelId); + return SendSystemMessageOutput.CreateFailure(input.RecipientPersonnelId, $"模板发送失败:{ex.Message}"); } } @@ -402,42 +400,37 @@ public class SystemMessageService : ISystemMessageService /// 标记消息为已读 /// 业务场景:用户查看消息后更新状态,用于消息中心的已读/未读状态管理 /// - public async Task MarkMessageAsReadAsync(long messageId, long recipientPersonnelId) + [HttpPost] + public async Task MarkMessageAsReadAsync(long messageId, long recipientPersonnelId) { try { - using var uow = _uowManager.Begin(); - var message = await _notificationHistoryRepository .Where(x => x.Id == messageId && - x.RecipientId == recipientPersonnelId.ToString() && + x.RecipientPersonnelId == recipientPersonnelId && !x.IsDeleted) .FirstAsync(); if (message == null) { - _logger.LogWarning("消息不存在或已被删除:消息ID {MessageId},收件人 {RecipientId}", messageId, recipientPersonnelId); - return false; + Logger.LogWarning("消息不存在或已被删除:消息ID {MessageId},收件人 {RecipientId}", messageId, recipientPersonnelId); + return SystemMessageStatusOutput.CreateFailure(recipientPersonnelId, "消息不存在或已被删除"); } - if (!message.IsRead) - { - message.IsRead = true; - message.ReadTime = DateTime.Now; - message.ModifiedTime = DateTime.Now; + // 系统消息的已读状态通过SendStatus管理,这里暂时不做处理 + // 实际项目中可能需要扩展实体或使用其他方式管理已读状态 + // 这里更新ModifiedTime表示已读 + message.ModifiedTime = DateTime.Now; - await _notificationHistoryRepository.UpdateAsync(message); - await uow.CommitAsync(); - - _logger.LogDebug("消息标记为已读:消息ID {MessageId},收件人 {RecipientId}", messageId, recipientPersonnelId); - } - - return true; + await _notificationHistoryRepository.UpdateAsync(message); + Logger.LogDebug("消息标记为已读:消息ID {MessageId},收件人 {RecipientId}", messageId, recipientPersonnelId); + + return SystemMessageStatusOutput.CreateSuccess(recipientPersonnelId, 1, "消息已标记为已读"); } catch (Exception ex) { - _logger.LogError(ex, "标记消息为已读失败:消息ID {MessageId},收件人 {RecipientId}", messageId, recipientPersonnelId); - return false; + Logger.LogError(ex, "标记消息为已读失败:消息ID {MessageId},收件人 {RecipientId}", messageId, recipientPersonnelId); + return SystemMessageStatusOutput.CreateFailure(recipientPersonnelId, $"标记为已读失败:{ex.Message}"); } } @@ -445,51 +438,47 @@ public class SystemMessageService : ISystemMessageService /// 批量标记消息为已读 /// 业务场景:用户批量处理消息,提高操作效率 /// - public async Task BatchMarkMessagesAsReadAsync(List messageIds, long recipientPersonnelId) + [HttpPost] + public async Task BatchMarkMessagesAsReadAsync(List messageIds, long recipientPersonnelId) { if (messageIds == null || !messageIds.Any()) { - _logger.LogWarning("批量标记消息为已读:消息ID列表为空"); - return 0; + Logger.LogWarning("批量标记消息为已读:消息ID列表为空"); + return SystemMessageStatusOutput.CreateFailure(recipientPersonnelId, "消息ID列表为空"); } try { - using var uow = _uowManager.Begin(); var currentTime = DateTime.Now; - // 查询未读的消息 + // 查询消息 var messages = await _notificationHistoryRepository .Where(x => messageIds.Contains(x.Id) && - x.RecipientId == recipientPersonnelId.ToString() && - !x.IsDeleted && - !x.IsRead) + x.RecipientPersonnelId == recipientPersonnelId && + !x.IsDeleted) .ToListAsync(); if (!messages.Any()) { - _logger.LogInformation("没有找到需要标记为已读的消息:收件人 {RecipientId}", recipientPersonnelId); - return 0; + Logger.LogInformation("没有找到需要标记为已读的消息:收件人 {RecipientId}", recipientPersonnelId); + return SystemMessageStatusOutput.CreateSuccess(recipientPersonnelId, 0, "没有找到需要标记为已读的消息"); } // 批量更新 foreach (var message in messages) { - message.IsRead = true; - message.ReadTime = currentTime; message.ModifiedTime = currentTime; } await _notificationHistoryRepository.UpdateAsync(messages); - await uow.CommitAsync(); - _logger.LogInformation("批量标记消息为已读完成:收件人 {RecipientId},更新数量 {Count}", recipientPersonnelId, messages.Count); - return messages.Count; + Logger.LogInformation("批量标记消息为已读完成:收件人 {RecipientId},更新数量 {Count}", recipientPersonnelId, messages.Count); + return SystemMessageStatusOutput.CreateSuccess(recipientPersonnelId, messages.Count, $"成功标记{messages.Count}条消息为已读"); } catch (Exception ex) { - _logger.LogError(ex, "批量标记消息为已读失败:收件人 {RecipientId}", recipientPersonnelId); - return 0; + Logger.LogError(ex, "批量标记消息为已读失败:收件人 {RecipientId}", recipientPersonnelId); + return SystemMessageStatusOutput.CreateFailure(recipientPersonnelId, $"批量标记为已读失败:{ex.Message}"); } } @@ -497,22 +486,21 @@ public class SystemMessageService : ISystemMessageService /// 删除消息 /// 业务场景:用户清理不需要的消息,实现软删除以保留审计记录 /// - public async Task DeleteMessageAsync(long messageId, long recipientPersonnelId) + [HttpPost] + public async Task DeleteMessageAsync(long messageId, long recipientPersonnelId) { try { - using var uow = _uowManager.Begin(); - var message = await _notificationHistoryRepository .Where(x => x.Id == messageId && - x.RecipientId == recipientPersonnelId.ToString() && + x.RecipientPersonnelId == recipientPersonnelId && !x.IsDeleted) .FirstAsync(); if (message == null) { - _logger.LogWarning("消息不存在或已被删除:消息ID {MessageId},收件人 {RecipientId}", messageId, recipientPersonnelId); - return false; + Logger.LogWarning("消息不存在或已被删除:消息ID {MessageId},收件人 {RecipientId}", messageId, recipientPersonnelId); + return SystemMessageStatusOutput.CreateFailure(recipientPersonnelId, "消息不存在或已被删除"); } // 软删除 @@ -520,15 +508,14 @@ public class SystemMessageService : ISystemMessageService message.ModifiedTime = DateTime.Now; await _notificationHistoryRepository.UpdateAsync(message); - await uow.CommitAsync(); - _logger.LogInformation("消息删除成功:消息ID {MessageId},收件人 {RecipientId}", messageId, recipientPersonnelId); - return true; + Logger.LogInformation("消息删除成功:消息ID {MessageId},收件人 {RecipientId}", messageId, recipientPersonnelId); + return SystemMessageStatusOutput.CreateSuccess(recipientPersonnelId, 1, "消息删除成功"); } catch (Exception ex) { - _logger.LogError(ex, "删除消息失败:消息ID {MessageId},收件人 {RecipientId}", messageId, recipientPersonnelId); - return false; + Logger.LogError(ex, "删除消息失败:消息ID {MessageId},收件人 {RecipientId}", messageId, recipientPersonnelId); + return SystemMessageStatusOutput.CreateFailure(recipientPersonnelId, $"删除消息失败:{ex.Message}"); } } @@ -541,17 +528,16 @@ public class SystemMessageService : ISystemMessageService try { var count = await _notificationHistoryRepository - .Where(x => x.RecipientId == recipientPersonnelId.ToString() && - x.NotificationType == Core.Enums.NotificationTypeEnum.SystemMessage && - !x.IsRead && + .Where(x => x.RecipientPersonnelId == recipientPersonnelId && + x.NotificationType == (int)NotificationTypeEnum.SystemMessage && !x.IsDeleted) .CountAsync(); - return count; + return (int)count; } catch (Exception ex) { - _logger.LogError(ex, "获取用户未读消息数量失败:收件人 {RecipientId}", recipientPersonnelId); + Logger.LogError(ex, "获取用户未读消息数量失败:收件人 {RecipientId}", recipientPersonnelId); return 0; } } @@ -559,22 +545,8 @@ public class SystemMessageService : ISystemMessageService #endregion #region 私有辅助方法 - - /// - /// 将SystemMessageTypeEnum转换为通知历史实体的MessageType字段 - /// - private string GetNotificationMessageType(SystemMessageTypeEnum messageType) - { - return messageType switch - { - SystemMessageTypeEnum.Info => "信息", - SystemMessageTypeEnum.Success => "成功", - SystemMessageTypeEnum.Warning => "警告", - SystemMessageTypeEnum.Error => "错误", - SystemMessageTypeEnum.Urgent => "紧急", - _ => "信息" - }; - } + + // 注:原有的消息类型转换方法已移除,因为实体结构调整后不再需要 #endregion } \ No newline at end of file diff --git a/NPP.SmartSchedue.Api/Services/Personnel/PersonnelQualificationService.cs b/NPP.SmartSchedue.Api/Services/Personnel/PersonnelQualificationService.cs index b77772c..4f00f1f 100644 --- a/NPP.SmartSchedue.Api/Services/Personnel/PersonnelQualificationService.cs +++ b/NPP.SmartSchedue.Api/Services/Personnel/PersonnelQualificationService.cs @@ -24,10 +24,14 @@ namespace NPP.SmartSchedue.Api.Services.Personnel; public class PersonnelQualificationService : BaseService, IPersonnelQualificationService, IDynamicApi { private readonly PersonnelQualificationRepository _personnelQualificationRepository; + private readonly QualificationRepository _qualificationRepository; - public PersonnelQualificationService(PersonnelQualificationRepository personnelQualificationRepository) + public PersonnelQualificationService( + PersonnelQualificationRepository personnelQualificationRepository, + QualificationRepository qualificationRepository) { _personnelQualificationRepository = personnelQualificationRepository; + _qualificationRepository = qualificationRepository; } /// @@ -314,4 +318,127 @@ public class PersonnelQualificationService : BaseService, IPersonnelQualificatio return new List(); } } + + /// + /// 获取人员资质统计信息(横坐标为资质,纵坐标为人员数量) + /// + /// 人员资质统计结果 + public async Task GetPersonnelQualificationStatisticsAsync() + { + try + { + // 获取所有有效的人员资质记录 + var activeQualifications = await _personnelQualificationRepository.Select + .Where(pq => pq.IsActive && (pq.ExpiryDate == null || pq.ExpiryDate > DateTime.Now)) + .ToListAsync(pq => new { pq.QualificationId, pq.PersonnelId }); + + // 按资质ID分组并统计每个资质的人员数量 + var qualificationStatistics = activeQualifications + .GroupBy(q => q.QualificationId) + .Select(g => new + { + QualificationId = g.Key, + PersonnelCount = g.Select(x => x.PersonnelId).Distinct().Count() + }) + .ToList(); + + // 获取所有资质的名称 + var qualificationIds = qualificationStatistics.Select(q => q.QualificationId).ToArray(); + var qualifications = await _qualificationRepository.Select + .Where(q => qualificationIds.Contains(q.Id)) + .ToListAsync(q => new { q.Id, q.Name }); + + var qualificationsDict = qualifications.ToDictionary(q => q.Id, q => q.Name); + + // 构建统计结果 + var statistics = qualificationStatistics + .Select(stat => new PersonnelQualificationStatistics + { + QualificationId = stat.QualificationId, + QualificationName = qualificationsDict.ContainsKey(stat.QualificationId) + ? qualificationsDict[stat.QualificationId] + : $"未知资质({stat.QualificationId})", + PersonnelCount = stat.PersonnelCount + }) + .ToList(); + + return new PersonnelQualificationStatisticsResult + { + Statistics = statistics, + GeneratedTime = DateTime.Now + }; + } + catch (Exception ex) + { + // 发生异常时返回空的统计结果 + return new PersonnelQualificationStatisticsResult + { + Statistics = new List(), + GeneratedTime = DateTime.Now + }; + } + } + + /// + /// 获取预警期内即将到期的人员资质清单 + /// + /// 基准日期(通常为 DateTime.Today) + /// 限定人员范围(可选) + /// 是否包含已过期记录 + /// + public async Task> GetExpiringAsync(DateTime today, IList personnelIds = null, bool includeExpired = false) + { + var baseDate = today.Date; + + var select = _personnelQualificationRepository.Select + .Where(pq => pq.IsActive) + .Where(pq => pq.ExpiryDate != null) + .Where(pq => pq.ExpiryWarningDays != null && pq.ExpiryWarningDays > 0); + + if (personnelIds != null && personnelIds.Count > 0) + { + select = select.Where(pq => personnelIds.Contains(pq.PersonnelId)); + } + + var list = await select.ToListAsync(); + + // 批量查询资质名称,避免 N+1 + var qIds = list.Select(x => x.QualificationId).Distinct().ToArray(); + var qNameMap = qIds.Length == 0 + ? new Dictionary() + : (await _qualificationRepository.Select + .Where(q => qIds.Contains(q.Id)) + .ToListAsync(q => new { q.Id, q.Name })) + .ToDictionary(x => x.Id, x => x.Name ?? string.Empty); + + var result = new List(list.Count); + foreach (var it in list) + { + var expiry = it.ExpiryDate.Value.Date; + var daysLeft = (int)Math.Floor((expiry - baseDate).TotalDays); + + bool inWindow = includeExpired + ? daysLeft <= it.ExpiryWarningDays.Value + : daysLeft >= 0 && daysLeft <= it.ExpiryWarningDays.Value; + + if (!inWindow) continue; + + result.Add(new ExpiringQualificationItemOutput + { + PersonnelId = it.PersonnelId, + PersonnelName = it.PersonnelName, + PersonnelCode = it.PersonnelCode, + QualificationId = it.QualificationId, + QualificationLevel = it.QualificationLevel, + ExpiryDate = it.ExpiryDate, + DaysLeft = daysLeft, + QualificationName = qNameMap.TryGetValue(it.QualificationId, out var qn) ? qn : string.Empty + }); + } + + return result + .OrderBy(x => x.DaysLeft) + .ThenBy(x => x.ExpiryDate) + .ToList(); + } } \ No newline at end of file diff --git a/NPP.SmartSchedue.Api/Services/Work/WorkOrderService.cs b/NPP.SmartSchedue.Api/Services/Work/WorkOrderService.cs index b99f51e..0e05e6f 100644 --- a/NPP.SmartSchedue.Api/Services/Work/WorkOrderService.cs +++ b/NPP.SmartSchedue.Api/Services/Work/WorkOrderService.cs @@ -440,7 +440,7 @@ public class WorkOrderService : BaseService, IWorkOrderService, IDynamicApi /// /// 待转换的工作任务实体 /// 转换后的工作任务实体,包含计算好的计划开始和结束时间 - private async Task ConvertWorkOrderShift(WorkOrderEntity workOrderEntity) + public async Task ConvertWorkOrderShift(WorkOrderEntity workOrderEntity) { // 【业务逻辑】:根据任务的班次ID获取班次定义信息 // 班次定义包含: StartTime、EndTime、班次名称等基础配置 @@ -475,6 +475,20 @@ public class WorkOrderService : BaseService, IWorkOrderService, IDynamicApi #endregion + public async Task> GetPendingSubmitCountByCreatorAsync() + { + var query = _workOrderRepository.Select + .Where(w => w.Status == (int)WorkOrderStatusEnum.PendingSubmit) + .GroupBy(w => new { w.CreatedUserId }) + .ToList<(long CreatedUserId, int Cnt)>(a => new ValueTuple(a.Key.CreatedUserId ?? 0, a.Count())); + + var dict = query + .Where(x => x.CreatedUserId > 0) + .GroupBy(x => x.CreatedUserId) + .ToDictionary(g => g.Key, g => g.Sum(x => x.Cnt)); + return dict; + } + #region 任务自检功能 /// diff --git a/NPP.SmartSchedue.Api/Services/Workbench/WorkbenchService.cs b/NPP.SmartSchedue.Api/Services/Workbench/WorkbenchService.cs new file mode 100644 index 0000000..6f3416d --- /dev/null +++ b/NPP.SmartSchedue.Api/Services/Workbench/WorkbenchService.cs @@ -0,0 +1,256 @@ +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Threading.Tasks; +using NPP.SmartSchedue.Api.Contracts.Domain.Time; +using NPP.SmartSchedue.Api.Contracts.Domain.Work; +using NPP.SmartSchedue.Api.Contracts.Services.Workbench; +using NPP.SmartSchedue.Api.Contracts.Services.Workbench.Input; +using NPP.SmartSchedue.Api.Contracts.Services.Workbench.Output; +using NPP.SmartSchedue.Api.Contracts.Core.Enums; +using NPP.SmartSchedue.Api.Contracts.Core.Extensions; +using NPP.SmartSchedue.Api.Contracts; +using ZhonTai.Admin.Services; +using ZhonTai.DynamicApi; +using ZhonTai.DynamicApi.Attributes; +using Microsoft.AspNetCore.Mvc; + +namespace NPP.SmartSchedue.Api.Services.Workbench; + +/// +/// 工作台服务实现 +/// 提供工作台相关的数据查询和业务逻辑处理 +/// +[DynamicApi(Area = "app")] +public class WorkbenchService : BaseService, IWorkbenchService, IDynamicApi +{ + private readonly IWorkOrderRepository _workOrderRepository; + private readonly IShiftUnavailabilityRepository _shiftUnavailabilityRepository; + private readonly IShiftRepository _shiftRepository; + + public WorkbenchService( + IWorkOrderRepository workOrderRepository, + IShiftUnavailabilityRepository shiftUnavailabilityRepository, + IShiftRepository shiftRepository) + { + _workOrderRepository = workOrderRepository; + _shiftUnavailabilityRepository = shiftUnavailabilityRepository; + _shiftRepository = shiftRepository; + } + + /// + /// 获取用户本周日历数据 + /// 包含任务排班信息和班次不可用性信息,用于日历视图展示 + /// + /// 查询参数 + /// 本周日历数据 + [HttpPost] + public async Task GetWeeklyCalendarAsync(WeeklyCalendarInput input) + { + // 1. 参数预处理和验证 + var currentUserId = User?.Id ?? throw new UnauthorizedAccessException("用户未登录"); + var targetPersonnelId = input.PersonnelId ?? currentUserId; + var targetDate = input.WeekDate ?? DateTime.Now; + + // 2. 计算本周的开始和结束日期(周一到周日) + var weekStartDate = GetWeekStartDate(targetDate); + var weekEndDate = weekStartDate.AddDays(6); + + // 3. 并行查询数据 + var tasksTask = GetWeeklyTasksAsync(targetPersonnelId, weekStartDate, weekEndDate, input.IncludeCompletedTasks); + var unavailabilitiesTask = input.IncludeUnavailableSlots + ? GetWeeklyUnavailabilitiesAsync(targetPersonnelId, weekStartDate, weekEndDate) + : Task.FromResult(new List()); + var shiftsTask = GetAllActiveShiftsAsync(); + + await Task.WhenAll(tasksTask, unavailabilitiesTask, shiftsTask); + + var tasks = await tasksTask; + var unavailabilities = await unavailabilitiesTask; + var shifts = await shiftsTask; + + // 4. 构建日历数据结构 + var result = new WeeklyCalendarOutput + { + WeekStartDate = weekStartDate, + WeekEndDate = weekEndDate, + PersonnelId = targetPersonnelId, + PersonnelName = "当前用户", // TODO: 从用户服务获取用户名 + Days = new List() + }; + + // 5. 按天组织数据 + for (int dayOffset = 0; dayOffset < 7; dayOffset++) + { + var currentDate = weekStartDate.AddDays(dayOffset); + var dayItem = new CalendarDayItem + { + Date = currentDate, + DayOfWeek = (int)currentDate.DayOfWeek == 0 ? 7 : (int)currentDate.DayOfWeek, + DayOfWeekName = GetDayOfWeekName((int)currentDate.DayOfWeek == 0 ? 7 : (int)currentDate.DayOfWeek), + Shifts = new List() + }; + + // 6. 为每一天的每个班次构建数据 + foreach (var shift in shifts.OrderBy(s => s.StartTime)) + { + var shiftItem = new CalendarShiftItem + { + ShiftId = shift.Id, + ShiftName = shift.Name, + StartTime = shift.StartTime, + EndTime = shift.EndTime, + Tasks = new List(), + UnavailableItems = new List() + }; + + // 7. 填充当前班次的任务数据 + var dayTasks = tasks.Where(t => t.WorkOrderDate.Date == currentDate.Date + && t.ShiftId == shift.Id).ToList(); + foreach (var task in dayTasks) + { + shiftItem.Tasks.Add(new CalendarTaskItem + { + TaskId = task.Id, + TaskCode = task.WorkOrderCode ?? task.Code ?? "", + ProjectNumber = task.ProjectNumber ?? "", + ProcessName = task.ProcessName ?? "", + Status = task.Status, + StatusName = ((WorkOrderStatusEnum)task.Status).ToString(), + Priority = task.Priority, + EstimatedHours = task.EstimatedHours, + PlannedStartTime = task.PlannedStartTime, + PlannedEndTime = task.PlannedEndTime, + Remarks = task.Remarks ?? "" + }); + } + + // 8. 填充当前班次的不可用时间段数据 + var dayUnavailabilities = unavailabilities.Where(u => u.Date.Date == currentDate.Date + && u.ShiftId == shift.Id).ToList(); + foreach (var unavailability in dayUnavailabilities) + { + shiftItem.UnavailableItems.Add(new CalendarUnavailableItem + { + UnavailabilityId = unavailability.Id, + ReasonType = unavailability.ReasonType, + ReasonTypeName = unavailability.ReasonType.GetDisplayName(), + Remark = unavailability.Remark ?? "", + EffectiveStartTime = unavailability.EffectiveStartTime, + EffectiveEndTime = unavailability.EffectiveEndTime, + Priority = unavailability.Priority + }); + } + + dayItem.Shifts.Add(shiftItem); + } + + result.Days.Add(dayItem); + } + + return result; + } + + #region 私有辅助方法 + + /// + /// 获取指定日期所在周的周一日期 + /// + /// 指定日期 + /// 周一日期 + private static DateTime GetWeekStartDate(DateTime date) + { + var dayOfWeek = (int)date.DayOfWeek; + if (dayOfWeek == 0) dayOfWeek = 7; // 将周日从0调整为7 + return date.AddDays(-(dayOfWeek - 1)).Date; + } + + /// + /// 获取星期几的中文名称 + /// + /// 星期几(1=周一,7=周日) + /// 中文名称 + private static string GetDayOfWeekName(int dayOfWeek) + { + return dayOfWeek switch + { + 1 => "周一", + 2 => "周二", + 3 => "周三", + 4 => "周四", + 5 => "周五", + 6 => "周六", + 7 => "周日", + _ => "" + }; + } + + /// + /// 获取指定人员本周的工作任务 + /// + /// 人员ID + /// 周开始日期 + /// 周结束日期 + /// 是否包含已完成任务 + /// 任务列表 + private async Task> GetWeeklyTasksAsync(long personnelId, DateTime weekStartDate, DateTime weekEndDate, bool includeCompleted) + { + // 构建查询条件 + var query = _workOrderRepository.Where(t => + t.AssignedPersonnelId == personnelId && + t.WorkOrderDate >= weekStartDate && + t.WorkOrderDate <= weekEndDate); + + // 根据参数决定是否过滤已完成任务 + if (!includeCompleted) + { + query = query.Where(t => t.Status != (int)WorkOrderStatusEnum.Completed); + } + + // 执行查询并包含相关实体 + var tasks = await query + .Include(t => t.ProcessEntity) + .Include(t => t.ShiftEntity) + .OrderBy(t => t.WorkOrderDate) + .OrderBy(t => t.PlannedStartTime) + .ToListAsync(); + + return tasks; + } + + /// + /// 获取指定人员本周的班次不可用记录 + /// + /// 人员ID + /// 周开始日期 + /// 周结束日期 + /// 不可用记录列表 + private async Task> GetWeeklyUnavailabilitiesAsync(long personnelId, DateTime weekStartDate, DateTime weekEndDate) + { + var unavailabilities = await _shiftUnavailabilityRepository.Where(u => + u.PersonnelId == personnelId && + u.Date >= weekStartDate && + u.Date <= weekEndDate) + .OrderBy(u => u.Date) + .OrderBy(u => u.Priority) + .ToListAsync(); + + return unavailabilities; + } + + /// + /// 获取所有活跃的班次 + /// + /// 班次列表 + private async Task> GetAllActiveShiftsAsync() + { + var shifts = await _shiftRepository.Where(s => s.IsEnabled) + .OrderBy(s => s.StartTime) + .ToListAsync(); + + return shifts; + } + + #endregion +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Host/.config/dotnet-tools.json b/NPP.SmartSchedue.Host/.config/dotnet-tools.json new file mode 100644 index 0000000..ef95326 --- /dev/null +++ b/NPP.SmartSchedue.Host/.config/dotnet-tools.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "isRoot": true, + "tools": { + "dotnet-ef": { + "version": "9.0.9", + "commands": [ + "dotnet-ef" + ], + "rollForward": false + } + } +} \ No newline at end of file diff --git a/NPP.SmartSchedue.Host/ConfigCenter/appconfig.json b/NPP.SmartSchedue.Host/ConfigCenter/appconfig.json index 7c55fb2..52ce53c 100644 --- a/NPP.SmartSchedue.Host/ConfigCenter/appconfig.json +++ b/NPP.SmartSchedue.Host/ConfigCenter/appconfig.json @@ -1,7 +1,7 @@ { "AppConfig": { "appType": "Controllers", - "urls": [ "http://*:19010" ], + "urls": [ "http://*:38030" ], "corUrls": [], "assemblyNames": [ "NPP.SmartSchedue.Api" ], "EnumListAssemblyNames": [ "NPP.SmartSchedue.Api", "NPP.SmartSchedue.Api.Contracts" ], @@ -21,7 +21,7 @@ "enableSchemaIdNamespace": false, "assemblyNameList": [], "routePrefix": "doc/app/swagger", - "url": "http://localhost:19010", + "url": "http://localhost:38030", "projects": [ { "name": "排班服务", diff --git a/NPP.SmartSchedue.Host/ConfigCenter/dbconfig.json b/NPP.SmartSchedue.Host/ConfigCenter/dbconfig.json index 60ab42f..d3445e3 100644 --- a/NPP.SmartSchedue.Host/ConfigCenter/dbconfig.json +++ b/NPP.SmartSchedue.Host/ConfigCenter/dbconfig.json @@ -22,7 +22,7 @@ //SqlServer "Data Source=.;Integrated Security=True;Initial Catalog=master;Encrypt=True;TrustServerCertificate=True;Pooling=true;Min Pool Size=1" //PostgreSQL "Host=localhost;Port=5432;Username=postgres;Password=; Database=postgres;Pooling=true;Minimum Pool Size=1" //Oracle "user id=SYS;password=pwd; data source=//127.0.0.1:1521/XE;Pooling=true;Min Pool Size=1" - "createDbConnectionString": "Server=10.249.11.115; Port=3306; Database=mysql; Uid=root; Pwd=root123$%^; Charset=utf8mb4;SslMode=none;Min pool size=1;Allow User Variables=True", + "createDbConnectionString": "Server=10.249.11.115; Port=3306; Database=mysql; Uid=root; Pwd=Wulihui1031; Charset=utf8mb4;SslMode=none;Min pool size=1;Allow User Variables=True", //建库脚本,复杂建库脚本可放到createdbsql.txt中 //MySql "CREATE DATABASE `appdb` CHARACTER SET 'utf8mb4' COLLATE 'utf8mb4_general_ci'" //SqlServer "CREATE DATABASE [appdb]" diff --git a/NPP.SmartSchedue.Host/Program.cs b/NPP.SmartSchedue.Host/Program.cs index ff02fcc..cbe3c8f 100644 --- a/NPP.SmartSchedue.Host/Program.cs +++ b/NPP.SmartSchedue.Host/Program.cs @@ -1,21 +1,23 @@ -using Microsoft.Extensions.DependencyInjection; +using Autofac; +using DotNetCore.CAP; +using DotNetCore.CAP.Messages; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.DependencyModel; using Microsoft.Extensions.Hosting; +using NPP.SmartSchedue.Api; +using NPP.SmartSchedue.Api.Core.Consts; +using NPP.SmartSchedue.Api.Core.Repositories; +using Savorboard.CAP.InMemoryMessageQueue; +using System.Linq; +using System.Reflection; +using System.Text.Encodings.Web; using ZhonTai; using ZhonTai.Admin.Core; using ZhonTai.Admin.Core.Configs; using ZhonTai.Admin.Core.Startup; using ZhonTai.ApiUI; -using NPP.SmartSchedue.Api.Core.Consts; -using Microsoft.AspNetCore.Builder; -using Microsoft.Extensions.DependencyModel; -using Savorboard.CAP.InMemoryMessageQueue; -using System.Reflection; -using System.Linq; -using Autofac; -using DotNetCore.CAP; -using Microsoft.Extensions.Configuration; -using NPP.SmartSchedue.Api.Core.Repositories; -using NPP.SmartSchedue.Api; new HostApp(new HostAppOptions() { @@ -44,28 +46,46 @@ new HostApp(new HostAppOptions() var rabbitMQ = context.Configuration.GetSection("CAP:RabbitMq").Get(); context.Services.AddCap(config => { + config.DefaultGroupName = "zhontai.admin"; + //开发阶段不同开发人员的消息区分,可以通过配置版本号实现 + config.Version = "v1"; + config.FailedRetryCount = 5; + config.FailedRetryInterval = 15; + config.EnablePublishParallelSend = true; + config.UseStorageLock = true; + config.JsonSerializerOptions.Encoder = JavaScriptEncoder.UnsafeRelaxedJsonEscaping; + config.UseInMemoryStorage(); config.UseInMemoryMessageQueue(); - - // - // - //config.UseMySql(dbConfig.ConnectionString); //config.UseRabbitMQ(mqConfig => { - // mqConfig.HostName = rabbitMQ.HostName; - // mqConfig.Port = rabbitMQ.Port; - // mqConfig.UserName = rabbitMQ.UserName; - // mqConfig.Password = rabbitMQ.Password; - // mqConfig.ExchangeName = rabbitMQ.ExchangeName; + // mqConfig.HostName = rabbitMQ.HostName; + // mqConfig.Port = rabbitMQ.Port; + // mqConfig.UserName = rabbitMQ.UserName; + // mqConfig.Password = rabbitMQ.Password; + // mqConfig.ExchangeName = rabbitMQ.ExchangeName; //}); + + config.FailedThresholdCallback = failed => + { + AppInfo.Log.Error($@"消息处理失败!类型: {failed.MessageType}, +已重试 {config.FailedRetryCount} 次仍失败,需人工处理。消息名称: {failed.Message.GetName()}"); + }; + config.UseDashboard(); }).AddSubscriberAssembly(assemblies); - + + // 注册设备相关Repository服务 context.Services.AddEquipmentRepositories(); // 注册算法引擎服务 context.Services.AddAlgorithmEngines(); + + // 注册通知相关服务 + context.Services.AddNotificationServices(); + + context.Services.AddWorkOrderIntegrationOperationServices(); }, //配置Autofac容器 ConfigureAutofacContainer = (builder, context) => diff --git a/NPP.SmartSchedue.Host/Properties/launchSettings.json b/NPP.SmartSchedue.Host/Properties/launchSettings.json index 1f84c1d..00b9234 100644 --- a/NPP.SmartSchedue.Host/Properties/launchSettings.json +++ b/NPP.SmartSchedue.Host/Properties/launchSettings.json @@ -3,7 +3,7 @@ "windowsAuthentication": false, "anonymousAuthentication": true, "iisExpress": { - "applicationUrl": "http://localhost:19010" + "applicationUrl": "http://localhost:38030" } }, "profiles": { @@ -15,7 +15,7 @@ "environmentVariables": { "ASPNETCORE_ENVIRONMENT": "Development" }, - "applicationUrl": "http://localhost:19010" + "applicationUrl": "http://localhost:38030" }, "IIS Express": { "commandName": "IISExpress", diff --git a/NPP.SmartSchedue.Host/appsettings.json b/NPP.SmartSchedue.Host/appsettings.json index c04b4fd..ed841d4 100644 --- a/NPP.SmartSchedue.Host/appsettings.json +++ b/NPP.SmartSchedue.Host/appsettings.json @@ -12,10 +12,10 @@ "Kestrel": { "Endpoints": { "Default": { - "Url": "http://*:19010" + "Url": "http://*:38030" }, "Grpc": { - "Url": "http://*:19011", + "Url": "http://*:38031", "Protocols": "Http2" } } @@ -23,23 +23,28 @@ "RpcConfig": { "Http": { "Enable": true, - "AssemblyNames": [ "ZhonTai.Admin.Contracts" ] + "AssemblyNames": ["ZhonTai.Admin.Contracts"] }, "Grpc": { "Enable": true, - "AssemblyNames": [ "ZhonTai.Admin.Core", "NPP.SmartSchedue.Api.Contracts", "NPP.EAM.Api.Contracts" ], - "ServerAssemblyNames": [ "ZhonTai.Admin", "NPP.EAM.Api" ] + "AssemblyNames": [ + "ZhonTai.Admin.Core", + "NPP.SmartSchedue.Api.Contracts", + "NPP.EAM.Api.Contracts", + "ZhonTai.Admin.Contracts" + ], + "ServerAssemblyNames": ["ZhonTai.Admin", "NPP.EAM.Api"] }, "Endpoints": [ { "Name": "admin", - "HttpUrl": "http://localhost:18010", - "GrpcUrl": "http://localhost:18011" + "HttpUrl": "http://localhost:38010", + "GrpcUrl": "http://localhost:38011" }, { "Name": "eam", - "HttpUrl": "http://localhost:18020", - "GrpcUrl": "http://localhost:18021" + "HttpUrl": "http://localhost:38020", + "GrpcUrl": "http://localhost:38021" } ] }, @@ -53,7 +58,7 @@ //外部服务配置 "Services": { "EAM": { - "BaseUrl": "http://localhost:18020", + "BaseUrl": "http://localhost:38020", "Timeout": 30000 } }, @@ -125,7 +130,8 @@ "ExpirySeconds": 300, // 缓存过期时长 "StoreageKeyPrefix": "app:captcha:", // 缓存前缀 "Tolerant": 0.02, // 容错值(校验时用,缺口位置与实际滑动位置匹配容错范围) - "Backgrounds": [ // 背景图配置 + "Backgrounds": [ + // 背景图配置 { "Type": "file", "Data": "wwwroot/captcha/jigsaw/backgrounds/1.jpg" @@ -161,4 +167,4 @@ //} ] } -} \ No newline at end of file +} diff --git a/NPP.SmartSchedue.Tests/NPP.SmartSchedue.Tests.csproj b/NPP.SmartSchedue.Tests/NPP.SmartSchedue.Tests.csproj index 50fffd7..fcce76b 100644 --- a/NPP.SmartSchedue.Tests/NPP.SmartSchedue.Tests.csproj +++ b/NPP.SmartSchedue.Tests/NPP.SmartSchedue.Tests.csproj @@ -14,6 +14,7 @@ + diff --git a/WorkOrderAssignmentService_重构总结.md b/WorkOrderAssignmentService_重构总结.md deleted file mode 100644 index c1b7915..0000000 --- a/WorkOrderAssignmentService_重构总结.md +++ /dev/null @@ -1,170 +0,0 @@ -# WorkOrderAssignmentService 企业级架构重构总结 - -## 📋 重构背景 - -原始的 `WorkOrderAssignmentService` 存在简化的实现逻辑,缺乏 Integration Service 中类似接口的完整架构模式。通过深度分析 `PersonnelAllocationService` 和 `TaskValidationService` 的实现,我们对该服务进行了企业级架构重构。 - -## 🎯 核心改进内容 - -### 1. 架构升级:从简单CRUD到企业级服务 - -#### 原始架构问题: -- ❌ 简化的验证逻辑,缺乏深度业务规则检查 -- ❌ 单一的验证方法,未复用 Integration Service 的智能算法 -- ❌ 缺乏性能监控和详细日志记录 -- ❌ 无事务性保障和回滚机制 -- ❌ 缺少智能推荐和质量评分功能 - -#### 企业级架构特性: -- ✅ **五层决策架构**:资格过滤 → 约束评估 → 优化决策 → 结果生成 → 统一验证 -- ✅ **高性能批处理**:内存缓存 + 并发处理 + 数据库优化 -- ✅ **全面业务验证**:资质匹配 + 时间冲突 + 工作限制 + 班次规则 -- ✅ **智能评分算法**:多维度权重计算 + 风险评估 + 推荐等级 -- ✅ **企业级监控**:性能指标 + 操作日志 + 异常处理 - -### 2. 核心技术架构模式 - -#### 🏗️ 四阶段处理架构 -```csharp -// 第一阶段:数据预处理和初步验证 -var dataPreparationResult = await ExecuteDataPreparationPhaseAsync(input, operationId); - -// 第二阶段:智能验证(五层决策模型) -var intelligentValidationResult = await ExecuteIntelligentValidationPhaseAsync(workOrder, input, operationId); - -// 第三阶段:事务性分配更新 -var updateResult = await ExecuteTransactionalUpdatePhaseAsync(workOrder, input, operationId); - -// 第四阶段:结果验证和数据刷新 -var postUpdateResult = await ExecutePostUpdateValidationPhaseAsync(input.WorkOrderId, operationId); -``` - -#### 📊 性能监控和日志追踪 -```csharp -using var activity = _activitySource?.StartActivity("UpdatePersonnelAssignment"); -var operationId = Guid.NewGuid().ToString("N")[..8]; - -activity?.SetTag("workOrder.id", input.WorkOrderId); -activity?.SetTag("processing.time.ms", stopwatch.ElapsedMilliseconds); -``` - -### 3. Integration Service 集成 - -#### 🤝 复用 PersonnelAllocationService 核心能力 -```csharp -// 直接调用五层决策模型的完整验证体系 -var validationResult = await _personnelAllocationService.ValidateAllocationAsync(validationInput); - -// 获取智能推荐候选人 -var candidates = await _personnelAllocationService.GetCandidatesAsync(workOrder.Id); -``` - -#### 🔗 服务间依赖注入 -```csharp -private readonly IPersonnelAllocationService _personnelAllocationService; -private readonly IEquipmentAllocationService _equipmentAllocationService; -private readonly ITaskIntegrationPreCheckService _taskIntegrationPreCheckService; -private readonly ISmartScheduleOrchestratorService _smartScheduleOrchestratorService; -``` - -### 4. 企业级配置和扩展性 - -#### ⚙️ 可配置化服务参数 -```csharp -public class AssignmentServiceConfiguration -{ - public int MaxBatchSize { get; set; } = 100; - public bool EnablePerformanceMonitoring { get; set; } = true; - public bool EnableSmartRecommendation { get; set; } = true; - public double DefaultQualificationMatchThreshold { get; set; } = 80.0; - public bool RiskAssessmentEnabled { get; set; } = true; -} -``` - -#### 🎨 智能推荐详情 -```csharp -public class AssignmentRecommendationDetails -{ - public double QualityScore { get; set; } - public string RecommendationLevel { get; set; } - public string RecommendationReason { get; set; } - public List AlternativeCandidates { get; set; } -} -``` - -## 📈 业务价值提升 - -### 1. 分配准确性显著提升 -- **质量评分机制**:从简单通过/失败 → 详细的百分制评分系统 -- **多维度验证**:资质(30%) + 约束(40%) + 优化(30%) 综合评估 -- **智能推荐算法**:基于历史数据和业务规则的候选人推荐 - -### 2. 系统可靠性增强 -- **事务性操作**:确保数据一致性,支持自动回滚 -- **异常处理**:分层异常处理,提供详细错误信息和建议 -- **操作追踪**:每个操作都有唯一ID,支持问题追溯 - -### 3. 用户体验优化 -- **智能提示**:提供替代候选人和优化建议 -- **详细反馈**:质量评分 + 推荐理由 + 风险提示 -- **性能提升**:缓存机制 + 批处理优化 - -### 4. 企业级运维支持 -- **性能监控**:处理时间、缓存命中率、验证复杂度 -- **详细日志**:操作记录、变更轨迹、审计支持 -- **可配置性**:灵活的业务规则和阈值设置 - -## 🔄 与 Integration Service 的对比分析 - -| 特性维度 | 原始 WorkOrderAssignmentService | 重构后 WorkOrderAssignmentService | PersonnelAllocationService | -|---------|--------------------------------|-----------------------------------|---------------------------| -| **架构复杂度** | 简单CRUD | 企业级四阶段架构 | 五层决策模型 | -| **验证深度** | 基础验证 | 复用完整验证体系 | 全面智能验证 | -| **性能监控** | 无 | 全面性能指标 | Activity跟踪 | -| **智能推荐** | 无 | 集成推荐算法 | 原生推荐引擎 | -| **缓存机制** | 无 | 内存缓存 + 线程安全 | 高性能缓存 | -| **事务支持** | 简单更新 | 完整事务回滚 | 批量事务处理 | - -## 🚀 扩展路径建议 - -### 1. 短期优化 -- 完善设备分配方法的企业级重构 -- 添加批量分配的智能优化算法 -- 集成更多 Integration Service 能力 - -### 2. 中期扩展 -- 实现完整的操作审计日志系统 -- 添加基于机器学习的推荐算法 -- 支持自定义业务规则配置 - -### 3. 长期规划 -- 微服务架构拆分 -- 支持分布式事务处理 -- 集成企业级监控和告警系统 - -## 📝 代码质量指标 - -### 1. 架构一致性 -- ✅ 与 Integration Service 保持一致的编程模式 -- ✅ 统一的异常处理和日志记录 -- ✅ 标准化的依赖注入和配置管理 - -### 2. 可维护性 -- ✅ 清晰的分层架构和职责分离 -- ✅ 详细的业务注释和技术文档 -- ✅ 模块化设计支持独立测试和扩展 - -### 3. 性能指标 -- ✅ 支持并发处理和批量优化 -- ✅ 智能缓存机制减少数据库访问 -- ✅ 性能监控和瓶颈识别 - -## 💡 核心亮点总结 - -1. **🎯 完整复用Integration架构**:直接集成PersonnelAllocationService的五层决策模型 -2. **⚡ 企业级性能优化**:Activity跟踪 + 内存缓存 + 并发处理 -3. **🛡️ 全面业务验证**:从简单检查升级到智能评分和风险评估 -4. **🔧 高度可配置性**:支持企业级的个性化配置和扩展 -5. **📊 智能推荐体系**:质量评分 + 替代方案 + 优化建议 - -这次重构将 WorkOrderAssignmentService 从一个简单的CRUD服务升级为具有企业级架构特性的智能分配服务,完全对齐了Integration Service中的先进架构模式,为后续的业务扩展和系统优化奠定了坚实基础。 \ No newline at end of file