This commit is contained in:
Asoka.Wang 2025-09-22 19:09:47 +08:00
parent 058d8edffa
commit 2b3f9acdce
100 changed files with 8837 additions and 1372 deletions

View File

@ -0,0 +1,63 @@
namespace NPP.SmartSchedue.Api.Contracts.Core.Configuration;
/// <summary>
/// 邮件配置
/// </summary>
public class EmailConfiguration
{
/// <summary>
/// 配置节点名称
/// </summary>
public const string SectionName = "EmailNotification";
/// <summary>
/// SMTP服务器地址
/// </summary>
public string SmtpServer { get; set; } = "";
/// <summary>
/// SMTP端口
/// </summary>
public int SmtpPort { get; set; } = 587;
/// <summary>
/// 发送者邮箱
/// </summary>
public string SenderEmail { get; set; } = "";
/// <summary>
/// 发送者密码
/// </summary>
public string SenderPassword { get; set; } = "";
/// <summary>
/// 发送者名称
/// </summary>
public string SenderName { get; set; } = "NPP智能生产调度系统";
/// <summary>
/// 是否启用SSL
/// </summary>
public bool EnableSsl { get; set; } = true;
/// <summary>
/// 超时时间(秒)
/// </summary>
public int TimeoutSeconds { get; set; } = 30;
/// <summary>
/// 是否启用邮件服务
/// </summary>
public bool Enabled { get; set; } = true;
/// <summary>
/// 验证配置是否有效
/// </summary>
public bool IsValid()
{
return !string.IsNullOrWhiteSpace(SmtpServer) &&
SmtpPort > 0 &&
!string.IsNullOrWhiteSpace(SenderEmail) &&
!string.IsNullOrWhiteSpace(SenderPassword);
}
}

View File

@ -0,0 +1,32 @@
namespace NPP.SmartSchedue.Api.Contracts.Core.Enums;
/// <summary>
/// 系统消息类型枚举
/// </summary>
public enum SystemMessageTypeEnum
{
/// <summary>
/// 信息
/// </summary>
Info = 1,
/// <summary>
/// 成功
/// </summary>
Success = 2,
/// <summary>
/// 警告
/// </summary>
Warning = 3,
/// <summary>
/// 错误
/// </summary>
Error = 4,
/// <summary>
/// 紧急
/// </summary>
Urgent = 5
}

View File

@ -38,5 +38,10 @@ public enum WorkOrderStatusEnum
/// <summary>
/// 已完成
/// </summary>
Completed = 7
Completed = 7,
/// <summary>
/// 已取消
/// </summary>
Cancelled = 99
}

View File

@ -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;
/// <summary>
/// 通知历史记录仓储接口
/// </summary>
public interface INotificationHistoryRepository : IRepositoryBase<NotificationHistoryEntity>
{
/// <summary>
/// 根据通知设置ID获取历史记录列表
/// </summary>
Task<List<NotificationHistoryEntity>> GetByNotificationSettingIdAsync(long notificationSettingId);
/// <summary>
/// 根据接收人员ID获取历史记录列表
/// </summary>
Task<List<NotificationHistoryEntity>> GetByRecipientPersonnelIdAsync(long recipientPersonnelId);
/// <summary>
/// 根据发送状态获取历史记录列表
/// </summary>
Task<List<NotificationHistoryEntity>> GetBySendStatusAsync(NotificationStatusEnum sendStatus);
/// <summary>
/// 根据通知方式获取历史记录列表
/// </summary>
Task<List<NotificationHistoryEntity>> GetByNotificationTypeAsync(NotificationTypeEnum notificationType);
/// <summary>
/// 根据业务类型和业务ID获取历史记录列表
/// </summary>
Task<List<NotificationHistoryEntity>> GetByBusinessAsync(string businessType, long? businessId = null);
/// <summary>
/// 获取需要重试的失败通知列表
/// </summary>
Task<List<NotificationHistoryEntity>> GetFailedNotificationsForRetryAsync(int? maxRetryCount = null);
/// <summary>
/// 获取指定时间范围内的通知统计信息
/// </summary>
Task<Dictionary<string, int>> GetNotificationStatisticsAsync(
DateTime startTime,
DateTime endTime,
long? notificationSettingId = null);
/// <summary>
/// 根据日期范围获取历史记录列表
/// </summary>
Task<List<NotificationHistoryEntity>> GetByDateRangeAsync(DateTime startDate, DateTime endDate);
/// <summary>
/// 更新通知发送状态
/// </summary>
Task UpdateSendStatusAsync(long id, NotificationStatusEnum sendStatus, string sendResult = "", string errorMessage = "");
/// <summary>
/// 批量更新通知发送状态
/// </summary>
Task BatchUpdateSendStatusAsync(List<(long Id, NotificationStatusEnum Status, string Result, string Error)> updates);
}

View File

@ -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;
/// <summary>
/// 通知设置仓储接口
/// </summary>
public interface INotificationSettingRepository : IRepositoryBase<NotificationSettingEntity>
{
/// <summary>
/// 根据启用状态获取通知设置列表
/// </summary>
Task<List<NotificationSettingEntity>> GetByEnabledAsync(bool enabled);
/// <summary>
/// 根据人员组ID获取通知设置列表
/// </summary>
Task<List<NotificationSettingEntity>> GetByPersonnelGroupIdAsync(long personnelGroupId);
/// <summary>
/// 根据通知方式获取通知设置列表
/// </summary>
Task<List<NotificationSettingEntity>> GetByNotificationTypeAsync(int notificationType);
/// <summary>
/// 根据触发条件获取匹配的通知设置列表
/// 这里实现一个简化的匹配逻辑,实际项目中可能需要更复杂的条件匹配引擎
/// </summary>
Task<List<NotificationSettingEntity>> GetMatchingNotificationSettingsAsync(
string businessType,
Dictionary<string, object> businessContext);
/// <summary>
/// 检查通知设置名称是否存在
/// </summary>
Task<bool> ExistsNotificationNameAsync(string notificationName, long? excludeId = null);
/// <summary>
/// 获取需要在当前时间执行的通知设置列表
/// 根据决策点2简单时间段只支持开始时间-结束时间
/// </summary>
Task<List<NotificationSettingEntity>> GetActiveNotificationSettingsForTimeAsync(DateTime currentTime);
}

View File

@ -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;
/// <summary>
/// 人员组仓储接口
/// </summary>
public interface IPersonnelGroupRepository : IRepositoryBase<PersonnelGroupEntity>
{
/// <summary>
/// 根据启用状态获取人员组列表
/// </summary>
Task<List<PersonnelGroupEntity>> GetByEnabledAsync(bool enabled);
/// <summary>
/// 根据人员组类型获取人员组列表
/// </summary>
Task<List<PersonnelGroupEntity>> GetByGroupTypeAsync(PersonnelGroupTypeEnum groupType);
/// <summary>
/// 检查人员组名称是否存在
/// </summary>
Task<bool> ExistsGroupNameAsync(string groupName, long? excludeId = null);
/// <summary>
/// 获取包含指定人员的人员组列表
/// </summary>
Task<List<PersonnelGroupEntity>> GetGroupsContainingPersonnelAsync(long personnelId);
/// <summary>
/// 获取包含指定部门的人员组列表
/// </summary>
Task<List<PersonnelGroupEntity>> GetGroupsContainingDepartmentAsync(long departmentId);
/// <summary>
/// 获取包含指定职位的人员组列表
/// </summary>
Task<List<PersonnelGroupEntity>> GetGroupsContainingPositionAsync(string position);
/// <summary>
/// 计算人员组的实际人员数量
/// 这个方法返回一个估算值实际计算需要在Service层进行
/// 因为需要查询人员表和考虑动态规则
/// </summary>
Task<int> CalculatePersonnelCountAsync(long personnelGroupId);
}

View File

@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
namespace NPP.SmartSchedue.Api.Contracts.Domain.Personnel
{
/// <summary>
/// 人员资质统计信息
/// </summary>
public class PersonnelQualificationStatistics
{
/// <summary>
/// 资质ID
/// </summary>
public long QualificationId { get; set; }
/// <summary>
/// 资质名称
/// </summary>
public string QualificationName { get; set; }
/// <summary>
/// 持有该资质的人员数量
/// </summary>
public int PersonnelCount { get; set; }
}
/// <summary>
/// 人员资质统计结果
/// </summary>
public class PersonnelQualificationStatisticsResult
{
/// <summary>
/// 统计数据列表
/// 横坐标为资质,纵坐标为人员数量
/// </summary>
public List<PersonnelQualificationStatistics> Statistics { get; set; } = new List<PersonnelQualificationStatistics>();
/// <summary>
/// 统计时间
/// </summary>
public DateTime GeneratedTime { get; set; } = DateTime.Now;
}
}

View File

@ -35,6 +35,7 @@
<ItemGroup>
<Folder Include="Domain\"/>
<Folder Include="Services\"/>
<Folder Include="Services\Workbench\Output\" />
</ItemGroup>
<Target Name="AfterTargetsBuild" AfterTargets="Build">

View File

@ -66,6 +66,18 @@ namespace NPP.SmartSchedue.Api.Contracts.Services.Integration
#endregion
#region
/// <summary>
/// 修改整合记录中的任务分配
/// 根据选中的人员修改任务的指定人员ID和姓名并更新整合记录
/// </summary>
/// <param name="input">任务修改输入参数</param>
/// <returns>修改结果</returns>
Task<bool> ModifyIntegrationRecordTaskAsync(IntegrationRecordTaskModifyInput input);
#endregion
#region
/// <summary>
@ -77,7 +89,7 @@ namespace NPP.SmartSchedue.Api.Contracts.Services.Integration
/// <param name="targetPersonnelIds">目标人员ID列表</param>
/// <param name="customMessage">自定义消息</param>
/// <returns>通知发送结果</returns>
Task<NotificationSendResult> SendPublishNotificationAsync(long recordId, string notificationType, List<long> targetPersonnelIds, string customMessage = "");
Task<IntegrationNotificationResult> SendPublishNotificationAsync(long recordId, string notificationType, List<long> targetPersonnelIds, string customMessage = "");
/// <summary>
/// 创建待办任务(预留接口)

View File

@ -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
{
/// <summary>
/// 修改整合记录中的任务分配
/// 根据选中的人员修改任务的指定人员ID和姓名并更新整合记录
/// </summary>
/// <param name="input">任务修改输入参数</param>
/// <returns>修改结果</returns>
Task<bool> ModifyIntegrationRecordTaskAsync(IntegrationRecordTaskModifyInput input);
}

View File

@ -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
{
/// <summary>
/// 工作订单整合操作统一服务
/// 简化版:专注核心业务流程,去除复杂的权限、版本、审计功能
/// </summary>
public interface IWorkOrderIntegrationOperationService
{
/// <summary>
/// 修改任务
/// </summary>
Task<WorkOrderOperationResult> ModifyWorkOrderAsync(WorkOrderModifyOperationInput input);
/// <summary>
/// 删除任务
/// </summary>
Task<WorkOrderOperationResult> DeleteWorkOrderAsync(WorkOrderDeleteOperationInput input);
}
}

View File

@ -0,0 +1,61 @@
using System.Collections.Generic;
namespace NPP.SmartSchedue.Api.Contracts.Services.Integration.Input
{
/// <summary>
/// 批量操作类型
/// </summary>
public enum BatchOperationType
{
/// <summary>
/// 批量修改
/// </summary>
BatchModify = 1,
/// <summary>
/// 批量删除
/// </summary>
BatchDelete = 2,
/// <summary>
/// 批量取消
/// </summary>
BatchCancel = 3
}
/// <summary>
/// 批量任务操作输入
/// </summary>
public class BatchWorkOrderOperationInput
{
/// <summary>
/// 操作类型
/// </summary>
public BatchOperationType OperationType { get; set; }
/// <summary>
/// 任务ID列表
/// </summary>
public List<long> TaskIds { get; set; } = new List<long>();
/// <summary>
/// 操作参数(根据操作类型不同,内容不同)
/// </summary>
public object OperationParameters { get; set; }
/// <summary>
/// 操作员用户ID
/// </summary>
public long OperatorUserId { get; set; }
/// <summary>
/// 操作员姓名
/// </summary>
public string OperatorName { get; set; }
/// <summary>
/// 操作备注
/// </summary>
public string Remarks { get; set; } = string.Empty;
}
}

View File

@ -0,0 +1,28 @@
namespace NPP.SmartSchedue.Api.Contracts.Services.Integration.Input
{
/// <summary>
/// 任务取消操作输入
/// </summary>
public class WorkOrderCancelOperationInput
{
/// <summary>
/// 任务ID
/// </summary>
public long TaskId { get; set; }
/// <summary>
/// 取消原因
/// </summary>
public string CancelReason { get; set; } = string.Empty;
/// <summary>
/// 操作员用户ID
/// </summary>
public long OperatorUserId { get; set; }
/// <summary>
/// 操作员姓名
/// </summary>
public string OperatorName { get; set; }
}
}

View File

@ -0,0 +1,33 @@
namespace NPP.SmartSchedue.Api.Contracts.Services.Integration.Input
{
/// <summary>
/// 任务删除操作输入
/// </summary>
public class WorkOrderDeleteOperationInput
{
/// <summary>
/// 任务ID
/// </summary>
public long TaskId { get; set; }
/// <summary>
/// 是否软删除true=软删除false=物理删除)
/// </summary>
public bool IsSoftDelete { get; set; } = true;
/// <summary>
/// 删除原因
/// </summary>
public string DeleteReason { get; set; } = string.Empty;
/// <summary>
/// 操作员用户ID
/// </summary>
public long OperatorUserId { get; set; }
/// <summary>
/// 操作员姓名
/// </summary>
public string OperatorName { get; set; }
}
}

View File

@ -0,0 +1,25 @@
using NPP.SmartSchedue.Api.Contracts.Services.Work.Input;
namespace NPP.SmartSchedue.Api.Contracts.Services.Integration.Input
{
/// <summary>
/// 任务修改操作输入
/// </summary>
public class WorkOrderModifyOperationInput
{
/// <summary>
/// 任务ID
/// </summary>
public long TaskId { get; set; }
/// <summary>
/// 修改的任务数据
/// </summary>
public WorkOrderUpdateInput TaskModifyData { get; set; }
/// <summary>
/// 操作备注
/// </summary>
public string Remarks { get; set; } = string.Empty;
}
}

View File

@ -0,0 +1,34 @@
namespace NPP.SmartSchedue.Api.Contracts.Services.Integration.Models
{
/// <summary>
/// 工作订单验证结果
/// </summary>
public class WorkOrderValidationResult
{
/// <summary>
/// 验证是否通过
/// </summary>
public bool IsValid { get; set; }
/// <summary>
/// 错误消息
/// </summary>
public string ErrorMessage { get; set; } = string.Empty;
/// <summary>
/// 创建成功结果
/// </summary>
public static WorkOrderValidationResult Success()
{
return new WorkOrderValidationResult { IsValid = true };
}
/// <summary>
/// 创建失败结果
/// </summary>
public static WorkOrderValidationResult Fail(string errorMessage)
{
return new WorkOrderValidationResult { IsValid = false, ErrorMessage = errorMessage };
}
}
}

View File

@ -0,0 +1,71 @@
using System.Collections.Generic;
namespace NPP.SmartSchedue.Api.Contracts.Services.Integration.Output
{
/// <summary>
/// 单个任务操作结果
/// </summary>
public class SingleTaskOperationResult
{
/// <summary>
/// 任务ID
/// </summary>
public long TaskId { get; set; }
/// <summary>
/// 操作是否成功
/// </summary>
public bool IsSuccess { get; set; }
/// <summary>
/// 操作消息
/// </summary>
public string Message { get; set; } = string.Empty;
/// <summary>
/// 错误详情
/// </summary>
public List<string> ErrorDetails { get; set; } = new List<string>();
}
/// <summary>
/// 批量操作结果输出
/// </summary>
public class BatchWorkOrderOperationResult
{
/// <summary>
/// 总体操作是否成功
/// </summary>
public bool IsSuccess { get; set; }
/// <summary>
/// 总体操作消息
/// </summary>
public string Message { get; set; } = string.Empty;
/// <summary>
/// 总任务数
/// </summary>
public int TotalCount { get; set; }
/// <summary>
/// 成功任务数
/// </summary>
public int SuccessCount { get; set; }
/// <summary>
/// 失败任务数
/// </summary>
public int FailedCount { get; set; }
/// <summary>
/// 每个任务的操作结果
/// </summary>
public List<SingleTaskOperationResult> TaskResults { get; set; } = new List<SingleTaskOperationResult>();
/// <summary>
/// 影响的整合记录ID列表
/// </summary>
public List<long> AffectedIntegrationRecordIds { get; set; } = new List<long>();
}
}

View File

@ -64,7 +64,7 @@ public class CancelIntegrationRecordResult
/// <summary>
/// 通知发送结果
/// </summary>
public List<NotificationSendResult> NotificationResults { get; set; } = new();
public List<IntegrationNotificationResult> NotificationResults { get; set; } = new();
/// <summary>
/// 撤销统计信息

View File

@ -86,7 +86,7 @@ namespace NPP.SmartSchedue.Api.Contracts.Services.Integration.Output
/// <summary>
/// 通知发送结果
/// </summary>
public List<NotificationSendResult> NotificationResults { get; set; } = new();
public List<IntegrationNotificationResult> NotificationResults { get; set; } = new();
/// <summary>
/// 警告信息

View File

@ -67,7 +67,7 @@ public class PublishIntegrationRecordResult
/// 通知发送结果
/// 记录发布后通知的发送情况
/// </summary>
public List<NotificationSendResult> NotificationResults { get; set; } = new();
public List<IntegrationNotificationResult> NotificationResults { get; set; } = new();
/// <summary>
/// 发布统计信息
@ -272,9 +272,9 @@ public class PublishValidationResult
}
/// <summary>
/// 通知发送结果
/// 整合通知发送结果
/// </summary>
public class NotificationSendResult
public class IntegrationNotificationResult
{
/// <summary>
/// 通知类型

View File

@ -87,7 +87,7 @@ namespace NPP.SmartSchedue.Api.Contracts.Services.Integration.Output
/// <summary>
/// 通知发送结果
/// </summary>
public List<NotificationSendResult> NotificationResults { get; set; } = new();
public List<IntegrationNotificationResult> NotificationResults { get; set; } = new();
/// <summary>
/// 验证结果

View File

@ -336,7 +336,7 @@ namespace NPP.SmartSchedue.Api.Contracts.Services.Integration.Output
/// <summary>
/// 通知发送结果
/// </summary>
public List<NotificationSendResult> NotificationResults { get; set; } = new();
public List<IntegrationNotificationResult> NotificationResults { get; set; } = new();
/// <summary>
/// 警告信息

View File

@ -0,0 +1,35 @@
using System.Collections.Generic;
namespace NPP.SmartSchedue.Api.Contracts.Services.Integration.Output
{
/// <summary>
/// 操作结果输出
/// </summary>
public class WorkOrderOperationResult
{
/// <summary>
/// 操作是否成功
/// </summary>
public bool IsSuccess { get; set; }
/// <summary>
/// 操作消息
/// </summary>
public string Message { get; set; } = string.Empty;
/// <summary>
/// 影响的整合记录ID
/// </summary>
public long? AffectedIntegrationRecordId { get; set; }
/// <summary>
/// 操作详情数据
/// </summary>
public object Data { get; set; }
/// <summary>
/// 错误详情(操作失败时)
/// </summary>
public List<string> ErrorDetails { get; set; } = new List<string>();
}
}

View File

@ -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;
/// </summary>
public interface IEmailNotificationService
{
#region
/// <summary>
/// 发送邮件通知
/// 发送单个邮件
/// </summary>
/// <param name="recipientEmail">接收人邮箱</param>
/// <param name="subject">邮件主题</param>
/// <param name="content">邮件内容</param>
/// <param name="isHtml">是否HTML格式</param>
/// <returns></returns>
Task<bool> SendEmailAsync(string recipientEmail, string subject, string content, bool isHtml = true);
Task<SendEmailOutput> SendEmailAsync(SendEmailInput input);
/// <summary>
/// 发送邮件通知(带附件)
/// 批量发送邮件
/// </summary>
/// <param name="recipientEmail">接收人邮箱</param>
/// <param name="subject">邮件主题</param>
/// <param name="content">邮件内容</param>
/// <param name="attachments">附件文件路径列表</param>
/// <param name="isHtml">是否HTML格式</param>
/// <returns></returns>
Task<bool> SendEmailWithAttachmentsAsync(
string recipientEmail,
string subject,
string content,
List<string> attachments,
bool isHtml = true);
#endregion
#region
/// <summary>
/// 批量发送邮件通知
/// </summary>
/// <param name="recipients">接收人邮箱列表</param>
/// <param name="subject">邮件主题</param>
/// <param name="content">邮件内容</param>
/// <param name="isHtml">是否HTML格式</param>
/// <returns>发送结果Key为邮箱地址Value为是否发送成功</returns>
Task<Dictionary<string, bool>> BatchSendEmailAsync(
List<string> recipients,
string subject,
string content,
bool isHtml = true);
Task<BatchSendEmailOutput> BatchSendEmailAsync(BatchSendEmailInput input);
/// <summary>
/// 个性化批量发送邮件通知
/// 每个收件人可以有不同的邮件内容
/// 个性化批量发送邮件
/// </summary>
/// <param name="emailItems">邮件项列表</param>
/// <returns>发送结果Key为邮箱地址Value为是否发送成功</returns>
Task<Dictionary<string, bool>> BatchSendPersonalizedEmailAsync(List<EmailItem> emailItems);
Task<BatchSendEmailOutput> BatchSendPersonalizedEmailAsync(BatchSendPersonalizedEmailInput input);
#endregion
#region
/// <summary>
/// 使用模板发送邮件
/// </summary>
/// <param name="recipientEmail">接收人邮箱</param>
/// <param name="subjectTemplate">邮件主题模板</param>
/// <param name="contentTemplate">邮件内容模板</param>
/// <param name="variables">模板变量</param>
/// <param name="isHtml">是否HTML格式</param>
/// <returns></returns>
Task<bool> SendEmailByTemplateAsync(
string recipientEmail,
string subjectTemplate,
string contentTemplate,
Dictionary<string, string> variables,
bool isHtml = true);
Task<SendEmailOutput> SendEmailByTemplateAsync(SendEmailByTemplateInput input);
#endregion
#region
/// <summary>
/// 验证邮箱地址格式
/// </summary>
/// <param name="email">邮箱地址</param>
/// <returns></returns>
bool IsValidEmail(string email);
Task<bool> IsValidEmailAsync(string email);
/// <summary>
/// 检查邮件服务器连接状态
/// </summary>
/// <returns></returns>
Task<bool> CheckEmailServerConnectionAsync();
#endregion
}
/// <summary>
/// 邮件项
/// </summary>
public class EmailItem
{
/// <summary>
/// 接收人邮箱
/// </summary>
public string RecipientEmail { get; set; } = "";
/// <summary>
/// 邮件主题
/// </summary>
public string Subject { get; set; } = "";
/// <summary>
/// 邮件内容
/// </summary>
public string Content { get; set; } = "";
/// <summary>
/// 是否HTML格式
/// </summary>
public bool IsHtml { get; set; } = true;
/// <summary>
/// 附件文件路径列表
/// </summary>
public List<string> Attachments { get; set; } = new List<string>();
/// <summary>
/// 个性化变量
/// </summary>
public Dictionary<string, string> Variables { get; set; } = new Dictionary<string, string>();
Task<EmailServerStatusOutput> CheckEmailServerStatusAsync();
}

View File

@ -29,7 +29,15 @@ public interface INotificationService
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
Task<PageOutput<NotificationSettingOutput>> GetNotificationSettingPageAsync(PageInput<NotificationSettingCreateInput> input);
Task<PageOutput<NotificationSettingOutput>> GetNotificationSettingPageAsync(PageInput<NotificationSettingPageInput> input);
/// <summary>
/// 获取通知设置列表(可选过滤,含缓存)
/// </summary>
/// <param name="isEnabled">是否启用(可选)</param>
/// <param name="personnelGroupId">人员组ID可选</param>
/// <returns></returns>
Task<List<NotificationSettingOutput>> GetNotificationSettingListAsync(bool? isEnabled = null, long? personnelGroupId = null);
/// <summary>
/// 创建通知设置
@ -76,7 +84,7 @@ public interface INotificationService
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
Task<PageOutput<PersonnelGroupOutput>> GetPersonnelGroupPageAsync(PageInput<PersonnelGroupCreateInput> input);
Task<PageOutput<PersonnelGroupOutput>> GetPersonnelGroupPageAsync(PageInput<PersonnelGroupGetPageInput> input);
/// <summary>
/// 创建人员组
@ -90,7 +98,7 @@ public interface INotificationService
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
Task UpdatePersonnelGroupAsync(PersonnelGroupCreateInput input);
Task UpdatePersonnelGroupAsync(PersonnelGroupUpdateInput input);
/// <summary>
/// 删除人员组
@ -119,6 +127,13 @@ public interface INotificationService
/// <returns></returns>
Task<SendNotificationOutput> SendNotificationAsync(SendNotificationInput input);
/// <summary>
/// 发送群组通知(邮件发送一封给所有人,系统消息仍然单独发送)
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
Task<SendNotificationOutput> SendGroupNotificationAsync(SendNotificationInput input);
/// <summary>
/// 批量发送通知
/// </summary>
@ -146,15 +161,6 @@ public interface INotificationService
#region 7
/// <summary>
/// 渲染通知模板
/// 支持通知内容模板,可替换变量
/// </summary>
/// <param name="template">模板内容</param>
/// <param name="variables">变量字典</param>
/// <returns></returns>
Task<string> RenderTemplateAsync(string template, Dictionary<string, string> variables);
/// <summary>
/// 验证模板语法
/// </summary>
@ -178,7 +184,7 @@ public interface INotificationService
/// </summary>
/// <param name="input"></param>
/// <returns></returns>
Task<PageOutput<NotificationHistoryOutput>> GetNotificationHistoryPageAsync(PageInput<NotificationHistoryOutput> input);
Task<PageOutput<NotificationHistoryOutput>> GetNotificationHistoryPageAsync(PageInput<NotificationHistoryPageInput> input);
/// <summary>
/// 重试失败的通知
@ -194,6 +200,20 @@ public interface INotificationService
/// <returns></returns>
Task<int> BatchRetryFailedNotificationsAsync(List<long> notificationHistoryIds);
/// <summary>
/// 取消通知
/// </summary>
/// <param name="notificationHistoryId">通知历史记录ID</param>
/// <returns></returns>
Task<bool> CancelNotificationAsync(long notificationHistoryId);
/// <summary>
/// 批量取消通知
/// </summary>
/// <param name="notificationHistoryIds">通知历史记录ID列表</param>
/// <returns>成功取消的数量</returns>
Task<int> BatchCancelNotificationsAsync(List<long> notificationHistoryIds);
#endregion
#region 6

View File

@ -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
/// 渲染通知模板
/// 支持变量替换,如:{变量名}
/// </summary>
/// <param name="template">模板内容</param>
/// <param name="variables">变量字典</param>
/// <returns>渲染后的内容</returns>
Task<string> RenderTemplateAsync(string template, Dictionary<string, string> variables);
/// <summary>
/// 同步渲染通知模板
/// </summary>
/// <param name="template">模板内容</param>
/// <param name="variables">变量字典</param>
/// <returns>渲染后的内容</returns>
string RenderTemplate(string template, Dictionary<string, string> variables);
/// <param name="input">渲染模板输入参数</param>
/// <returns>渲染结果</returns>
Task<RenderTemplateOutput> RenderTemplateAsync(RenderTemplateInput input);
#endregion
@ -35,16 +27,9 @@ public interface INotificationTemplateService
/// <summary>
/// 验证模板语法是否正确
/// </summary>
/// <param name="template">模板内容</param>
/// <param name="input">验证模板输入参数</param>
/// <returns>验证结果</returns>
Task<TemplateValidationResult> ValidateTemplateAsync(string template);
/// <summary>
/// 同步验证模板语法
/// </summary>
/// <param name="template">模板内容</param>
/// <returns>验证结果</returns>
TemplateValidationResult ValidateTemplate(string template);
Task<ValidateTemplateOutput> ValidateTemplateAsync(ValidateTemplateInput input);
#endregion
@ -53,16 +38,9 @@ public interface INotificationTemplateService
/// <summary>
/// 提取模板中的变量列表
/// </summary>
/// <param name="template">模板内容</param>
/// <returns>变量名列表</returns>
Task<List<string>> ExtractVariablesAsync(string template);
/// <summary>
/// 同步提取模板中的变量列表
/// </summary>
/// <param name="template">模板内容</param>
/// <returns>变量名列表</returns>
List<string> ExtractVariables(string template);
/// <param name="input">提取变量输入参数</param>
/// <returns>变量列表</returns>
Task<ExtractVariablesOutput> ExtractVariablesAsync(ExtractVariablesInput input);
#endregion
@ -71,20 +49,15 @@ public interface INotificationTemplateService
/// <summary>
/// 获取系统内置变量
/// </summary>
/// <returns>内置变量字典</returns>
Task<Dictionary<string, string>> GetSystemVariablesAsync();
/// <returns>系统变量字典</returns>
Task<GetSystemVariablesOutput> GetSystemVariablesAsync();
/// <summary>
/// 获取业务相关变量
/// </summary>
/// <param name="businessType">业务类型</param>
/// <param name="businessId">业务ID</param>
/// <param name="businessData">业务数据</param>
/// <param name="input">获取业务变量输入参数</param>
/// <returns>业务变量字典</returns>
Task<Dictionary<string, string>> GetBusinessVariablesAsync(
string businessType,
long? businessId = null,
string businessData = "");
Task<GetBusinessVariablesOutput> GetBusinessVariablesAsync(GetBusinessVariablesInput input);
#endregion
@ -93,148 +66,17 @@ public interface INotificationTemplateService
/// <summary>
/// 获取预定义模板列表
/// </summary>
/// <param name="category">模板分类</param>
/// <param name="input">获取模板列表输入参数</param>
/// <returns>预定义模板列表</returns>
Task<List<PredefinedTemplate>> GetPredefinedTemplatesAsync(string category = "");
Task<GetPredefinedTemplatesOutput> GetPredefinedTemplatesAsync(GetPredefinedTemplatesInput input);
/// <summary>
/// 获取指定预定义模板
/// </summary>
/// <param name="templateId">模板ID</param>
/// <param name="input">获取指定模板输入参数</param>
/// <returns>预定义模板</returns>
Task<PredefinedTemplate> GetPredefinedTemplateAsync(string templateId);
Task<GetPredefinedTemplateOutput> GetPredefinedTemplateAsync(GetPredefinedTemplateInput input);
#endregion
}
/// <summary>
/// 模板验证结果
/// </summary>
public class TemplateValidationResult
{
/// <summary>
/// 是否验证通过
/// </summary>
public bool IsValid { get; set; } = true;
/// <summary>
/// 错误信息列表
/// </summary>
public List<string> Errors { get; set; } = new List<string>();
/// <summary>
/// 警告信息列表
/// </summary>
public List<string> Warnings { get; set; } = new List<string>();
/// <summary>
/// 发现的变量列表
/// </summary>
public List<string> Variables { get; set; } = new List<string>();
/// <summary>
/// 添加错误信息
/// </summary>
/// <param name="error">错误信息</param>
public void AddError(string error)
{
IsValid = false;
Errors.Add(error);
}
/// <summary>
/// 添加警告信息
/// </summary>
/// <param name="warning">警告信息</param>
public void AddWarning(string warning)
{
Warnings.Add(warning);
}
}
/// <summary>
/// 预定义模板
/// </summary>
public class PredefinedTemplate
{
/// <summary>
/// 模板ID
/// </summary>
public string TemplateId { get; set; } = "";
/// <summary>
/// 模板名称
/// </summary>
public string TemplateName { get; set; } = "";
/// <summary>
/// 模板分类
/// </summary>
public string Category { get; set; } = "";
/// <summary>
/// 模板描述
/// </summary>
public string Description { get; set; } = "";
/// <summary>
/// 邮件主题模板
/// </summary>
public string EmailSubjectTemplate { get; set; } = "";
/// <summary>
/// 邮件内容模板
/// </summary>
public string EmailContentTemplate { get; set; } = "";
/// <summary>
/// 系统消息标题模板
/// </summary>
public string SystemMessageTitleTemplate { get; set; } = "";
/// <summary>
/// 系统消息内容模板
/// </summary>
public string SystemMessageContentTemplate { get; set; } = "";
/// <summary>
/// 支持的变量列表
/// </summary>
public List<TemplateVariable> SupportedVariables { get; set; } = new List<TemplateVariable>();
}
/// <summary>
/// 模板变量定义
/// </summary>
public class TemplateVariable
{
/// <summary>
/// 变量名
/// </summary>
public string Name { get; set; } = "";
/// <summary>
/// 变量描述
/// </summary>
public string Description { get; set; } = "";
/// <summary>
/// 变量类型
/// </summary>
public string Type { get; set; } = "string";
/// <summary>
/// 是否必需
/// </summary>
public bool IsRequired { get; set; } = false;
/// <summary>
/// 默认值
/// </summary>
public string DefaultValue { get; set; } = "";
/// <summary>
/// 示例值
/// </summary>
public string ExampleValue { get; set; } = "";
}

View File

@ -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
/// <summary>
/// 发送系统消息
/// </summary>
/// <param name="recipientPersonnelId">接收人员ID</param>
/// <param name="title">消息标题</param>
/// <param name="content">消息内容</param>
/// <param name="messageType">消息类型(通知、警告、错误等)</param>
/// <param name="businessType">业务类型</param>
/// <param name="businessId">业务ID</param>
/// <returns></returns>
Task<bool> SendSystemMessageAsync(
long recipientPersonnelId,
string title,
string content,
SystemMessageTypeEnum messageType = SystemMessageTypeEnum.Info,
string businessType = "",
long? businessId = null);
/// <param name="input">发送系统消息输入模型</param>
/// <returns>发送结果</returns>
Task<SendSystemMessageOutput> SendSystemMessageAsync(SendSystemMessageInput input);
/// <summary>
/// 发送带操作按钮的系统消息
/// </summary>
/// <param name="recipientPersonnelId">接收人员ID</param>
/// <param name="title">消息标题</param>
/// <param name="content">消息内容</param>
/// <param name="actions">操作按钮列表</param>
/// <param name="messageType">消息类型</param>
/// <param name="businessType">业务类型</param>
/// <param name="businessId">业务ID</param>
/// <returns></returns>
Task<bool> SendSystemMessageWithActionsAsync(
long recipientPersonnelId,
string title,
string content,
List<SystemMessageAction> actions,
SystemMessageTypeEnum messageType = SystemMessageTypeEnum.Info,
string businessType = "",
long? businessId = null);
/// <param name="input">发送带操作按钮的系统消息输入模型</param>
/// <returns>发送结果</returns>
Task<SendSystemMessageOutput> SendSystemMessageWithActionsAsync(SendSystemMessageWithActionsInput input);
#endregion
@ -57,28 +34,17 @@ public interface ISystemMessageService
/// <summary>
/// 批量发送系统消息
/// </summary>
/// <param name="recipientPersonnelIds">接收人员ID列表</param>
/// <param name="title">消息标题</param>
/// <param name="content">消息内容</param>
/// <param name="messageType">消息类型</param>
/// <param name="businessType">业务类型</param>
/// <param name="businessId">业务ID</param>
/// <returns>发送结果Key为人员IDValue为是否发送成功</returns>
Task<Dictionary<long, bool>> BatchSendSystemMessageAsync(
List<long> recipientPersonnelIds,
string title,
string content,
SystemMessageTypeEnum messageType = SystemMessageTypeEnum.Info,
string businessType = "",
long? businessId = null);
/// <param name="input">批量发送系统消息输入模型</param>
/// <returns>批量发送结果</returns>
Task<BatchSendSystemMessageOutput> BatchSendSystemMessageAsync(BatchSendSystemMessageInput input);
/// <summary>
/// 个性化批量发送系统消息
/// 每个收件人可以有不同的消息内容
/// </summary>
/// <param name="messageItems">消息项列表</param>
/// <returns>发送结果Key为人员IDValue为是否发送成功</returns>
Task<Dictionary<long, bool>> BatchSendPersonalizedSystemMessageAsync(List<SystemMessageItem> messageItems);
/// <returns>批量发送结果</returns>
Task<BatchSendSystemMessageOutput> BatchSendPersonalizedSystemMessageAsync(List<SystemMessageItem> messageItems);
#endregion
@ -87,22 +53,9 @@ public interface ISystemMessageService
/// <summary>
/// 使用模板发送系统消息
/// </summary>
/// <param name="recipientPersonnelId">接收人员ID</param>
/// <param name="titleTemplate">消息标题模板</param>
/// <param name="contentTemplate">消息内容模板</param>
/// <param name="variables">模板变量</param>
/// <param name="messageType">消息类型</param>
/// <param name="businessType">业务类型</param>
/// <param name="businessId">业务ID</param>
/// <returns></returns>
Task<bool> SendSystemMessageByTemplateAsync(
long recipientPersonnelId,
string titleTemplate,
string contentTemplate,
Dictionary<string, string> variables,
SystemMessageTypeEnum messageType = SystemMessageTypeEnum.Info,
string businessType = "",
long? businessId = null);
/// <param name="input">使用模板发送系统消息输入模型</param>
/// <returns>发送结果</returns>
Task<SendSystemMessageOutput> SendSystemMessageByTemplateAsync(SendSystemMessageByTemplateInput input);
#endregion
@ -113,139 +66,31 @@ public interface ISystemMessageService
/// </summary>
/// <param name="messageId">消息ID</param>
/// <param name="recipientPersonnelId">接收人员ID</param>
/// <returns></returns>
Task<bool> MarkMessageAsReadAsync(long messageId, long recipientPersonnelId);
/// <returns>操作结果</returns>
Task<SystemMessageStatusOutput> MarkMessageAsReadAsync(long messageId, long recipientPersonnelId);
/// <summary>
/// 批量标记消息为已读
/// </summary>
/// <param name="messageIds">消息ID列表</param>
/// <param name="recipientPersonnelId">接收人员ID</param>
/// <returns></returns>
Task<int> BatchMarkMessagesAsReadAsync(List<long> messageIds, long recipientPersonnelId);
/// <returns>操作结果</returns>
Task<SystemMessageStatusOutput> BatchMarkMessagesAsReadAsync(List<long> messageIds, long recipientPersonnelId);
/// <summary>
/// 删除消息
/// </summary>
/// <param name="messageId">消息ID</param>
/// <param name="recipientPersonnelId">接收人员ID</param>
/// <returns></returns>
Task<bool> DeleteMessageAsync(long messageId, long recipientPersonnelId);
/// <returns>操作结果</returns>
Task<SystemMessageStatusOutput> DeleteMessageAsync(long messageId, long recipientPersonnelId);
/// <summary>
/// 获取用户未读消息数量
/// </summary>
/// <param name="recipientPersonnelId">接收人员ID</param>
/// <returns></returns>
/// <returns>未读消息数量</returns>
Task<int> GetUnreadMessageCountAsync(long recipientPersonnelId);
#endregion
}
/// <summary>
/// 系统消息类型枚举
/// </summary>
public enum SystemMessageTypeEnum
{
/// <summary>
/// 信息
/// </summary>
Info = 1,
/// <summary>
/// 成功
/// </summary>
Success = 2,
/// <summary>
/// 警告
/// </summary>
Warning = 3,
/// <summary>
/// 错误
/// </summary>
Error = 4,
/// <summary>
/// 紧急
/// </summary>
Urgent = 5
}
/// <summary>
/// 系统消息项
/// </summary>
public class SystemMessageItem
{
/// <summary>
/// 接收人员ID
/// </summary>
public long RecipientPersonnelId { get; set; }
/// <summary>
/// 消息标题
/// </summary>
public string Title { get; set; } = "";
/// <summary>
/// 消息内容
/// </summary>
public string Content { get; set; } = "";
/// <summary>
/// 消息类型
/// </summary>
public SystemMessageTypeEnum MessageType { get; set; } = SystemMessageTypeEnum.Info;
/// <summary>
/// 业务类型
/// </summary>
public string BusinessType { get; set; } = "";
/// <summary>
/// 业务ID
/// </summary>
public long? BusinessId { get; set; }
/// <summary>
/// 操作按钮列表
/// </summary>
public List<SystemMessageAction> Actions { get; set; } = new List<SystemMessageAction>();
/// <summary>
/// 个性化变量
/// </summary>
public Dictionary<string, string> Variables { get; set; } = new Dictionary<string, string>();
}
/// <summary>
/// 系统消息操作按钮
/// </summary>
public class SystemMessageAction
{
/// <summary>
/// 操作ID
/// </summary>
public string ActionId { get; set; } = "";
/// <summary>
/// 操作名称
/// </summary>
public string ActionName { get; set; } = "";
/// <summary>
/// 操作URL
/// </summary>
public string ActionUrl { get; set; } = "";
/// <summary>
/// 操作类型(按钮、链接等)
/// </summary>
public string ActionType { get; set; } = "button";
/// <summary>
/// 是否主要操作
/// </summary>
public bool IsPrimary { get; set; } = false;
}

View File

@ -0,0 +1,35 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Input;
/// <summary>
/// 批量发送邮件输入
/// </summary>
public class BatchSendEmailInput
{
/// <summary>
/// 接收人邮箱列表
/// </summary>
[Required(ErrorMessage = "接收人邮箱列表不能为空")]
[MinLength(1, ErrorMessage = "至少需要一个接收人")]
public List<string> Recipients { get; set; } = new List<string>();
/// <summary>
/// 邮件主题
/// </summary>
[Required(ErrorMessage = "邮件主题不能为空")]
[MaxLength(500, ErrorMessage = "邮件主题长度不能超过500个字符")]
public string Subject { get; set; } = "";
/// <summary>
/// 邮件内容
/// </summary>
[Required(ErrorMessage = "邮件内容不能为空")]
public string Content { get; set; } = "";
/// <summary>
/// 是否HTML格式
/// </summary>
public bool IsHtml { get; set; } = true;
}

View File

@ -0,0 +1,53 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Input;
/// <summary>
/// 个性化批量发送邮件输入
/// </summary>
public class BatchSendPersonalizedEmailInput
{
/// <summary>
/// 邮件项列表
/// </summary>
[Required(ErrorMessage = "邮件项列表不能为空")]
[MinLength(1, ErrorMessage = "至少需要一个邮件项")]
public List<EmailItem> EmailItems { get; set; } = new List<EmailItem>();
}
/// <summary>
/// 邮件项
/// </summary>
public class EmailItem
{
/// <summary>
/// 接收人邮箱
/// </summary>
[Required(ErrorMessage = "接收人邮箱不能为空")]
[EmailAddress(ErrorMessage = "邮箱地址格式无效")]
public string RecipientEmail { get; set; } = "";
/// <summary>
/// 邮件主题
/// </summary>
[Required(ErrorMessage = "邮件主题不能为空")]
[MaxLength(500, ErrorMessage = "邮件主题长度不能超过500个字符")]
public string Subject { get; set; } = "";
/// <summary>
/// 邮件内容
/// </summary>
[Required(ErrorMessage = "邮件内容不能为空")]
public string Content { get; set; } = "";
/// <summary>
/// 是否HTML格式
/// </summary>
public bool IsHtml { get; set; } = true;
/// <summary>
/// 附件文件路径列表
/// </summary>
public List<string>? Attachments { get; set; }
}

View File

@ -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;
/// <summary>
/// 批量发送系统消息输入模型
/// </summary>
public class BatchSendSystemMessageInput
{
/// <summary>
/// 接收人员ID列表
/// </summary>
[Required(ErrorMessage = "接收人员ID列表不能为空")]
public List<long> RecipientPersonnelIds { get; set; } = new List<long>();
/// <summary>
/// 消息标题
/// </summary>
[Required(ErrorMessage = "消息标题不能为空")]
[StringLength(200, ErrorMessage = "消息标题长度不能超过200个字符")]
public string Title { get; set; } = "";
/// <summary>
/// 消息内容
/// </summary>
[Required(ErrorMessage = "消息内容不能为空")]
[StringLength(4000, ErrorMessage = "消息内容长度不能超过4000个字符")]
public string Content { get; set; } = "";
/// <summary>
/// 消息类型
/// </summary>
public SystemMessageTypeEnum MessageType { get; set; } = SystemMessageTypeEnum.Info;
/// <summary>
/// 业务类型
/// </summary>
[StringLength(100, ErrorMessage = "业务类型长度不能超过100个字符")]
public string BusinessType { get; set; } = "";
/// <summary>
/// 业务ID
/// </summary>
public long? BusinessId { get; set; }
}

View File

@ -0,0 +1,15 @@
using System.ComponentModel.DataAnnotations;
namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Input;
/// <summary>
/// 提取变量输入模型
/// </summary>
public class ExtractVariablesInput
{
/// <summary>
/// 模板内容
/// </summary>
[Required(ErrorMessage = "模板内容不能为空")]
public string Template { get; set; } = "";
}

View File

@ -0,0 +1,25 @@
using System.ComponentModel.DataAnnotations;
namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Input;
/// <summary>
/// 获取业务变量输入模型
/// </summary>
public class GetBusinessVariablesInput
{
/// <summary>
/// 业务类型
/// </summary>
[Required(ErrorMessage = "业务类型不能为空")]
public string BusinessType { get; set; } = "";
/// <summary>
/// 业务ID
/// </summary>
public long? BusinessId { get; set; }
/// <summary>
/// 业务数据
/// </summary>
public string BusinessData { get; set; } = "";
}

View File

@ -0,0 +1,15 @@
using System.ComponentModel.DataAnnotations;
namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Input;
/// <summary>
/// 获取指定预定义模板输入模型
/// </summary>
public class GetPredefinedTemplateInput
{
/// <summary>
/// 模板ID
/// </summary>
[Required(ErrorMessage = "模板ID不能为空")]
public string TemplateId { get; set; } = "";
}

View File

@ -0,0 +1,12 @@
namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Input;
/// <summary>
/// 获取预定义模板列表输入模型
/// </summary>
public class GetPredefinedTemplatesInput
{
/// <summary>
/// 模板分类
/// </summary>
public string Category { get; set; } = "";
}

View File

@ -0,0 +1,47 @@
using System;
using NPP.SmartSchedue.Api.Contracts.Core.Enums;
namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Input;
public class NotificationHistoryPageInput
{
/// <summary>
/// 通知设置ID
/// </summary>
public long? NotificationSettingId { get; set; }
/// <summary>
/// 接收人员ID
/// </summary>
public long? RecipientPersonnelId { get; set; }
/// <summary>
/// 接收人员姓名
/// </summary>
public string RecipientPersonnelName { get; set; } = "";
/// <summary>
/// 通知方式
/// </summary>
public NotificationTypeEnum? NotificationType { get; set; }
/// <summary>
/// 发送状态
/// </summary>
public NotificationStatusEnum? SendStatus { get; set; }
/// <summary>
/// 业务类型
/// </summary>
public string BusinessType { get; set; } = "";
/// <summary>
/// 开始时间
/// </summary>
public DateTime? StartTime { get; set; }
/// <summary>
/// 结束时间
/// </summary>
public DateTime? EndTime { get; set; }
}

View File

@ -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; }
}

View File

@ -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; } = "";
/// <summary>
/// 人员组类型
/// </summary>
public PersonnelGroupTypeEnum? GroupType { get; set; }
/// <summary>
/// 是否启用
/// </summary>
public bool? IsEnabled { get; set; }
}

View File

@ -0,0 +1,16 @@
using System.ComponentModel.DataAnnotations;
using NPP.SmartSchedue.Api.Contracts.Core.Enums;
namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Input;
/// <summary>
/// 更新人员组输入
/// </summary>
public class PersonnelGroupUpdateInput : PersonnelGroupCreateInput
{
/// <summary>
/// 人员组ID
/// </summary>
[Required(ErrorMessage = "人员组ID不能为空")]
public long Id { get; set; }
}

View File

@ -0,0 +1,21 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Input;
/// <summary>
/// 渲染模板输入模型
/// </summary>
public class RenderTemplateInput
{
/// <summary>
/// 模板内容
/// </summary>
[Required(ErrorMessage = "模板内容不能为空")]
public string Template { get; set; } = "";
/// <summary>
/// 变量字典
/// </summary>
public Dictionary<string, string> Variables { get; set; } = new Dictionary<string, string>();
}

View File

@ -0,0 +1,39 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Input;
/// <summary>
/// 使用模板发送邮件输入
/// </summary>
public class SendEmailByTemplateInput
{
/// <summary>
/// 接收人邮箱
/// </summary>
[Required(ErrorMessage = "接收人邮箱不能为空")]
[EmailAddress(ErrorMessage = "邮箱地址格式无效")]
public string RecipientEmail { get; set; } = "";
/// <summary>
/// 邮件主题模板
/// </summary>
[Required(ErrorMessage = "邮件主题模板不能为空")]
public string SubjectTemplate { get; set; } = "";
/// <summary>
/// 邮件内容模板
/// </summary>
[Required(ErrorMessage = "邮件内容模板不能为空")]
public string ContentTemplate { get; set; } = "";
/// <summary>
/// 模板变量
/// </summary>
public Dictionary<string, string> Variables { get; set; } = new Dictionary<string, string>();
/// <summary>
/// 是否HTML格式
/// </summary>
public bool IsHtml { get; set; } = true;
}

View File

@ -0,0 +1,40 @@
using System.Collections.Generic;
using System.ComponentModel.DataAnnotations;
namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Input;
/// <summary>
/// 发送邮件输入
/// </summary>
public class SendEmailInput
{
/// <summary>
/// 接收人邮箱
/// </summary>
[Required(ErrorMessage = "接收人邮箱不能为空")]
[EmailAddress(ErrorMessage = "邮箱地址格式无效")]
public string RecipientEmail { get; set; } = "";
/// <summary>
/// 邮件主题
/// </summary>
[Required(ErrorMessage = "邮件主题不能为空")]
[MaxLength(500, ErrorMessage = "邮件主题长度不能超过500个字符")]
public string Subject { get; set; } = "";
/// <summary>
/// 邮件内容
/// </summary>
[Required(ErrorMessage = "邮件内容不能为空")]
public string Content { get; set; } = "";
/// <summary>
/// 是否HTML格式
/// </summary>
public bool IsHtml { get; set; } = true;
/// <summary>
/// 附件文件路径列表
/// </summary>
public List<string>? Attachments { get; set; }
}

View File

@ -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
/// </summary>
[Required(ErrorMessage = "通知方式不能为空")]
public NotificationTypeEnum NotificationType { get; set; }
public long SettingId { get; set; }
/// <summary>
/// 通知标题
/// </summary>

View File

@ -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;
/// <summary>
/// 使用模板发送系统消息输入模型
/// </summary>
public class SendSystemMessageByTemplateInput
{
/// <summary>
/// 接收人员ID
/// </summary>
[Required(ErrorMessage = "接收人员ID不能为空")]
[Range(1, long.MaxValue, ErrorMessage = "接收人员ID必须大于0")]
public long RecipientPersonnelId { get; set; }
/// <summary>
/// 消息标题模板
/// </summary>
[Required(ErrorMessage = "消息标题模板不能为空")]
[StringLength(200, ErrorMessage = "消息标题模板长度不能超过200个字符")]
public string TitleTemplate { get; set; } = "";
/// <summary>
/// 消息内容模板
/// </summary>
[Required(ErrorMessage = "消息内容模板不能为空")]
[StringLength(4000, ErrorMessage = "消息内容模板长度不能超过4000个字符")]
public string ContentTemplate { get; set; } = "";
/// <summary>
/// 模板变量
/// </summary>
public Dictionary<string, string> Variables { get; set; } = new Dictionary<string, string>();
/// <summary>
/// 消息类型
/// </summary>
public SystemMessageTypeEnum MessageType { get; set; } = SystemMessageTypeEnum.Info;
/// <summary>
/// 业务类型
/// </summary>
[StringLength(100, ErrorMessage = "业务类型长度不能超过100个字符")]
public string BusinessType { get; set; } = "";
/// <summary>
/// 业务ID
/// </summary>
public long? BusinessId { get; set; }
}

View File

@ -0,0 +1,49 @@
using System.ComponentModel.DataAnnotations;
using NPP.SmartSchedue.Api.Contracts.Core.Enums;
namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Input;
/// <summary>
/// 发送系统消息输入模型
/// </summary>
public class SendSystemMessageInput
{
/// <summary>
/// 接收人员ID
/// </summary>
[Required(ErrorMessage = "接收人员ID不能为空")]
[Range(1, long.MaxValue, ErrorMessage = "接收人员ID必须大于0")]
public long RecipientPersonnelId { get; set; }
public long NotificationSettingId { get; set; }
/// <summary>
/// 消息标题
/// </summary>
[Required(ErrorMessage = "消息标题不能为空")]
[StringLength(200, ErrorMessage = "消息标题长度不能超过200个字符")]
public string Title { get; set; } = "";
/// <summary>
/// 消息内容
/// </summary>
[Required(ErrorMessage = "消息内容不能为空")]
[StringLength(4000, ErrorMessage = "消息内容长度不能超过4000个字符")]
public string Content { get; set; } = "";
/// <summary>
/// 消息类型
/// </summary>
public SystemMessageTypeEnum MessageType { get; set; } = SystemMessageTypeEnum.Info;
/// <summary>
/// 业务类型
/// </summary>
[StringLength(100, ErrorMessage = "业务类型长度不能超过100个字符")]
public string BusinessType { get; set; } = "";
/// <summary>
/// 业务ID
/// </summary>
public long? BusinessId { get; set; }
}

View File

@ -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;
/// <summary>
/// 发送带操作按钮的系统消息输入模型
/// </summary>
public class SendSystemMessageWithActionsInput
{
/// <summary>
/// 接收人员ID
/// </summary>
[Required(ErrorMessage = "接收人员ID不能为空")]
[Range(1, long.MaxValue, ErrorMessage = "接收人员ID必须大于0")]
public long RecipientPersonnelId { get; set; }
/// <summary>
/// 消息标题
/// </summary>
[Required(ErrorMessage = "消息标题不能为空")]
[StringLength(200, ErrorMessage = "消息标题长度不能超过200个字符")]
public string Title { get; set; } = "";
/// <summary>
/// 消息内容
/// </summary>
[Required(ErrorMessage = "消息内容不能为空")]
[StringLength(4000, ErrorMessage = "消息内容长度不能超过4000个字符")]
public string Content { get; set; } = "";
/// <summary>
/// 操作按钮列表
/// </summary>
public List<SystemMessageAction> Actions { get; set; } = new List<SystemMessageAction>();
/// <summary>
/// 消息类型
/// </summary>
public SystemMessageTypeEnum MessageType { get; set; } = SystemMessageTypeEnum.Info;
/// <summary>
/// 业务类型
/// </summary>
[StringLength(100, ErrorMessage = "业务类型长度不能超过100个字符")]
public string BusinessType { get; set; } = "";
/// <summary>
/// 业务ID
/// </summary>
public long? BusinessId { get; set; }
}

View File

@ -0,0 +1,15 @@
using System.ComponentModel.DataAnnotations;
namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Input;
/// <summary>
/// 验证模板输入模型
/// </summary>
public class ValidateTemplateInput
{
/// <summary>
/// 模板内容
/// </summary>
[Required(ErrorMessage = "模板内容不能为空")]
public string Template { get; set; } = "";
}

View File

@ -0,0 +1,45 @@
using System;
using System.Collections.Generic;
namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Output;
/// <summary>
/// 批量发送邮件输出
/// </summary>
public class BatchSendEmailOutput
{
/// <summary>
/// 总发送数量
/// </summary>
public int TotalCount { get; set; }
/// <summary>
/// 成功发送数量
/// </summary>
public int SuccessCount { get; set; }
/// <summary>
/// 失败发送数量
/// </summary>
public int FailedCount { get; set; }
/// <summary>
/// 发送时间
/// </summary>
public DateTime SendTime { get; set; } = DateTime.Now;
/// <summary>
/// 详细发送结果
/// </summary>
public List<SendEmailOutput> Results { get; set; } = new List<SendEmailOutput>();
/// <summary>
/// 整体是否成功(所有邮件都发送成功)
/// </summary>
public bool IsAllSuccess => FailedCount == 0;
/// <summary>
/// 成功率
/// </summary>
public decimal SuccessRate => TotalCount > 0 ? (decimal)SuccessCount / TotalCount : 0;
}

View File

@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Output;
/// <summary>
/// 批量发送系统消息输出模型
/// </summary>
public class BatchSendSystemMessageOutput
{
/// <summary>
/// 总发送数量
/// </summary>
public int TotalCount { get; set; }
/// <summary>
/// 成功发送数量
/// </summary>
public int SuccessCount { get; set; }
/// <summary>
/// 失败发送数量
/// </summary>
public int FailureCount { get; set; }
/// <summary>
/// 详细发送结果
/// Key: 人员ID, Value: 发送结果
/// </summary>
public Dictionary<long, SendSystemMessageOutput> Results { get; set; } = new Dictionary<long, SendSystemMessageOutput>();
/// <summary>
/// 批量发送时间
/// </summary>
public DateTime SentTime { get; set; }
/// <summary>
/// 是否全部发送成功
/// </summary>
public bool AllSuccess => FailureCount == 0;
/// <summary>
/// 成功率(百分比)
/// </summary>
public double SuccessRate => TotalCount > 0 ? (double)SuccessCount / TotalCount * 100 : 0;
/// <summary>
/// 创建批量发送结果
/// </summary>
public static BatchSendSystemMessageOutput Create(Dictionary<long, SendSystemMessageOutput> 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;
}
}

View File

@ -0,0 +1,39 @@
using System.Collections.Generic;
namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Output;
/// <summary>
/// 邮件项
/// </summary>
public class EmailItem
{
/// <summary>
/// 接收人邮箱
/// </summary>
public string RecipientEmail { get; set; } = "";
/// <summary>
/// 邮件主题
/// </summary>
public string Subject { get; set; } = "";
/// <summary>
/// 邮件内容
/// </summary>
public string Content { get; set; } = "";
/// <summary>
/// 是否HTML格式
/// </summary>
public bool IsHtml { get; set; } = true;
/// <summary>
/// 附件文件路径列表
/// </summary>
public List<string> Attachments { get; set; } = new List<string>();
/// <summary>
/// 个性化变量
/// </summary>
public Dictionary<string, string> Variables { get; set; } = new Dictionary<string, string>();
}

View File

@ -0,0 +1,39 @@
using System;
namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Output;
/// <summary>
/// 邮件服务器状态输出
/// </summary>
public class EmailServerStatusOutput
{
/// <summary>
/// 服务器是否可用
/// </summary>
public bool IsAvailable { get; set; }
/// <summary>
/// SMTP服务器地址
/// </summary>
public string SmtpServer { get; set; } = "";
/// <summary>
/// SMTP端口
/// </summary>
public int SmtpPort { get; set; }
/// <summary>
/// 检查时间
/// </summary>
public DateTime CheckTime { get; set; } = DateTime.Now;
/// <summary>
/// 响应时间(毫秒)
/// </summary>
public int ResponseTimeMs { get; set; }
/// <summary>
/// 错误消息
/// </summary>
public string? ErrorMessage { get; set; }
}

View File

@ -0,0 +1,24 @@
using System.Collections.Generic;
namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Output;
/// <summary>
/// 提取变量输出模型
/// </summary>
public class ExtractVariablesOutput
{
/// <summary>
/// 提取到的变量列表
/// </summary>
public List<string> Variables { get; set; } = new List<string>();
/// <summary>
/// 是否提取成功
/// </summary>
public bool Success { get; set; } = true;
/// <summary>
/// 错误信息
/// </summary>
public string ErrorMessage { get; set; } = "";
}

View File

@ -0,0 +1,24 @@
using System.Collections.Generic;
namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Output;
/// <summary>
/// 获取业务变量输出模型
/// </summary>
public class GetBusinessVariablesOutput
{
/// <summary>
/// 业务变量字典
/// </summary>
public Dictionary<string, string> Variables { get; set; } = new Dictionary<string, string>();
/// <summary>
/// 是否获取成功
/// </summary>
public bool Success { get; set; } = true;
/// <summary>
/// 错误信息
/// </summary>
public string ErrorMessage { get; set; } = "";
}

View File

@ -0,0 +1,22 @@
namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Output;
/// <summary>
/// 获取指定预定义模板输出模型
/// </summary>
public class GetPredefinedTemplateOutput
{
/// <summary>
/// 预定义模板
/// </summary>
public PredefinedTemplate? Template { get; set; }
/// <summary>
/// 是否获取成功
/// </summary>
public bool Success { get; set; } = true;
/// <summary>
/// 错误信息
/// </summary>
public string ErrorMessage { get; set; } = "";
}

View File

@ -0,0 +1,29 @@
using System.Collections.Generic;
namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Output;
/// <summary>
/// 获取预定义模板列表输出模型
/// </summary>
public class GetPredefinedTemplatesOutput
{
/// <summary>
/// 预定义模板列表
/// </summary>
public List<PredefinedTemplate> Templates { get; set; } = new List<PredefinedTemplate>();
/// <summary>
/// 模板总数
/// </summary>
public int TotalCount { get; set; }
/// <summary>
/// 是否获取成功
/// </summary>
public bool Success { get; set; } = true;
/// <summary>
/// 错误信息
/// </summary>
public string ErrorMessage { get; set; } = "";
}

View File

@ -0,0 +1,24 @@
using System.Collections.Generic;
namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Output;
/// <summary>
/// 获取系统变量输出模型
/// </summary>
public class GetSystemVariablesOutput
{
/// <summary>
/// 系统变量字典
/// </summary>
public Dictionary<string, string> Variables { get; set; } = new Dictionary<string, string>();
/// <summary>
/// 是否获取成功
/// </summary>
public bool Success { get; set; } = true;
/// <summary>
/// 错误信息
/// </summary>
public string ErrorMessage { get; set; } = "";
}

View File

@ -0,0 +1,54 @@
using System.Collections.Generic;
namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Output;
/// <summary>
/// 预定义模板
/// </summary>
public class PredefinedTemplate
{
/// <summary>
/// 模板ID
/// </summary>
public string TemplateId { get; set; } = "";
/// <summary>
/// 模板名称
/// </summary>
public string TemplateName { get; set; } = "";
/// <summary>
/// 模板分类
/// </summary>
public string Category { get; set; } = "";
/// <summary>
/// 模板描述
/// </summary>
public string Description { get; set; } = "";
/// <summary>
/// 邮件主题模板
/// </summary>
public string EmailSubjectTemplate { get; set; } = "";
/// <summary>
/// 邮件内容模板
/// </summary>
public string EmailContentTemplate { get; set; } = "";
/// <summary>
/// 系统消息标题模板
/// </summary>
public string SystemMessageTitleTemplate { get; set; } = "";
/// <summary>
/// 系统消息内容模板
/// </summary>
public string SystemMessageContentTemplate { get; set; } = "";
/// <summary>
/// 支持的变量列表
/// </summary>
public List<TemplateVariable> SupportedVariables { get; set; } = new List<TemplateVariable>();
}

View File

@ -0,0 +1,22 @@
namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Output;
/// <summary>
/// 渲染模板输出模型
/// </summary>
public class RenderTemplateOutput
{
/// <summary>
/// 渲染后的内容
/// </summary>
public string RenderedContent { get; set; } = "";
/// <summary>
/// 是否渲染成功
/// </summary>
public bool Success { get; set; } = true;
/// <summary>
/// 错误信息
/// </summary>
public string ErrorMessage { get; set; } = "";
}

View File

@ -0,0 +1,34 @@
using System;
namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Output;
/// <summary>
/// 发送邮件输出
/// </summary>
public class SendEmailOutput
{
/// <summary>
/// 是否发送成功
/// </summary>
public bool IsSuccess { get; set; }
/// <summary>
/// 错误消息
/// </summary>
public string? ErrorMessage { get; set; }
/// <summary>
/// 发送时间
/// </summary>
public DateTime SendTime { get; set; } = DateTime.Now;
/// <summary>
/// 接收人邮箱
/// </summary>
public string RecipientEmail { get; set; } = "";
/// <summary>
/// 邮件主题
/// </summary>
public string Subject { get; set; } = "";
}

View File

@ -0,0 +1,61 @@
using System;
namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Output;
/// <summary>
/// 发送系统消息输出模型
/// </summary>
public class SendSystemMessageOutput
{
/// <summary>
/// 是否发送成功
/// </summary>
public bool Success { get; set; }
/// <summary>
/// 消息ID发送成功时返回
/// </summary>
public long? MessageId { get; set; }
/// <summary>
/// 错误消息(发送失败时返回)
/// </summary>
public string ErrorMessage { get; set; } = "";
/// <summary>
/// 发送时间
/// </summary>
public DateTime? SentTime { get; set; }
/// <summary>
/// 接收人员ID
/// </summary>
public long RecipientPersonnelId { get; set; }
/// <summary>
/// 创建成功结果
/// </summary>
public static SendSystemMessageOutput CreateSuccess(long messageId, long recipientPersonnelId, DateTime sentTime)
{
return new SendSystemMessageOutput
{
Success = true,
MessageId = messageId,
RecipientPersonnelId = recipientPersonnelId,
SentTime = sentTime
};
}
/// <summary>
/// 创建失败结果
/// </summary>
public static SendSystemMessageOutput CreateFailure(long recipientPersonnelId, string errorMessage)
{
return new SendSystemMessageOutput
{
Success = false,
RecipientPersonnelId = recipientPersonnelId,
ErrorMessage = errorMessage
};
}
}

View File

@ -0,0 +1,32 @@
namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Output;
/// <summary>
/// 系统消息操作按钮
/// </summary>
public class SystemMessageAction
{
/// <summary>
/// 操作ID
/// </summary>
public string ActionId { get; set; } = "";
/// <summary>
/// 操作名称
/// </summary>
public string ActionName { get; set; } = "";
/// <summary>
/// 操作URL
/// </summary>
public string ActionUrl { get; set; } = "";
/// <summary>
/// 操作类型(按钮、链接等)
/// </summary>
public string ActionType { get; set; } = "button";
/// <summary>
/// 是否主要操作
/// </summary>
public bool IsPrimary { get; set; } = false;
}

View File

@ -0,0 +1,50 @@
using System.Collections.Generic;
using NPP.SmartSchedue.Api.Contracts.Core.Enums;
namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Output;
/// <summary>
/// 系统消息项
/// </summary>
public class SystemMessageItem
{
/// <summary>
/// 接收人员ID
/// </summary>
public long RecipientPersonnelId { get; set; }
/// <summary>
/// 消息标题
/// </summary>
public string Title { get; set; } = "";
/// <summary>
/// 消息内容
/// </summary>
public string Content { get; set; } = "";
/// <summary>
/// 消息类型
/// </summary>
public SystemMessageTypeEnum MessageType { get; set; } = SystemMessageTypeEnum.Info;
/// <summary>
/// 业务类型
/// </summary>
public string BusinessType { get; set; } = "";
/// <summary>
/// 业务ID
/// </summary>
public long? BusinessId { get; set; }
/// <summary>
/// 操作按钮列表
/// </summary>
public List<SystemMessageAction> Actions { get; set; } = new List<SystemMessageAction>();
/// <summary>
/// 个性化变量
/// </summary>
public Dictionary<string, string> Variables { get; set; } = new Dictionary<string, string>();
}

View File

@ -0,0 +1,64 @@
using System;
namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Output;
/// <summary>
/// 系统消息状态输出模型
/// </summary>
public class SystemMessageStatusOutput
{
/// <summary>
/// 操作是否成功
/// </summary>
public bool Success { get; set; }
/// <summary>
/// 操作结果消息
/// </summary>
public string Message { get; set; } = "";
/// <summary>
/// 影响的消息数量
/// </summary>
public int AffectedCount { get; set; }
/// <summary>
/// 操作时间
/// </summary>
public DateTime OperationTime { get; set; }
/// <summary>
/// 人员ID
/// </summary>
public long PersonnelId { get; set; }
/// <summary>
/// 创建成功结果
/// </summary>
public static SystemMessageStatusOutput CreateSuccess(long personnelId, int affectedCount, string message = "操作成功")
{
return new SystemMessageStatusOutput
{
Success = true,
Message = message,
AffectedCount = affectedCount,
PersonnelId = personnelId,
OperationTime = DateTime.Now
};
}
/// <summary>
/// 创建失败结果
/// </summary>
public static SystemMessageStatusOutput CreateFailure(long personnelId, string errorMessage)
{
return new SystemMessageStatusOutput
{
Success = false,
Message = errorMessage,
AffectedCount = 0,
PersonnelId = personnelId,
OperationTime = DateTime.Now
};
}
}

View File

@ -0,0 +1,48 @@
using System.Collections.Generic;
namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Output;
/// <summary>
/// 模板验证结果
/// </summary>
public class TemplateValidationResult
{
/// <summary>
/// 是否验证通过
/// </summary>
public bool IsValid { get; set; } = true;
/// <summary>
/// 错误信息列表
/// </summary>
public List<string> Errors { get; set; } = new List<string>();
/// <summary>
/// 警告信息列表
/// </summary>
public List<string> Warnings { get; set; } = new List<string>();
/// <summary>
/// 发现的变量列表
/// </summary>
public List<string> Variables { get; set; } = new List<string>();
/// <summary>
/// 添加错误信息
/// </summary>
/// <param name="error">错误信息</param>
public void AddError(string error)
{
IsValid = false;
Errors.Add(error);
}
/// <summary>
/// 添加警告信息
/// </summary>
/// <param name="warning">警告信息</param>
public void AddWarning(string warning)
{
Warnings.Add(warning);
}
}

View File

@ -0,0 +1,37 @@
namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Output;
/// <summary>
/// 模板变量定义
/// </summary>
public class TemplateVariable
{
/// <summary>
/// 变量名
/// </summary>
public string Name { get; set; } = "";
/// <summary>
/// 变量描述
/// </summary>
public string Description { get; set; } = "";
/// <summary>
/// 变量类型
/// </summary>
public string Type { get; set; } = "string";
/// <summary>
/// 是否必需
/// </summary>
public bool IsRequired { get; set; } = false;
/// <summary>
/// 默认值
/// </summary>
public string DefaultValue { get; set; } = "";
/// <summary>
/// 示例值
/// </summary>
public string ExampleValue { get; set; } = "";
}

View File

@ -0,0 +1,24 @@
using System.Collections.Generic;
namespace NPP.SmartSchedue.Api.Contracts.Services.Notification.Output;
/// <summary>
/// 验证模板输出模型
/// </summary>
public class ValidateTemplateOutput
{
/// <summary>
/// 验证结果详情
/// </summary>
public TemplateValidationResult ValidationResult { get; set; } = new TemplateValidationResult();
/// <summary>
/// 是否验证成功
/// </summary>
public bool Success { get; set; } = true;
/// <summary>
/// 错误信息
/// </summary>
public string ErrorMessage { get; set; } = "";
}

View File

@ -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
/// <param name="personnelId">人员ID</param>
/// <returns>人员有效资质实体列表</returns>
Task<List<NPP.SmartSchedue.Api.Contracts.Domain.Personnel.PersonnelQualificationEntity>> GetPersonnelQualificationsAsync(long personnelId);
/// <summary>
/// 获取人员资质统计信息(横坐标为资质,纵坐标为人员数量)
/// </summary>
/// <returns>人员资质统计结果</returns>
Task<PersonnelQualificationStatisticsResult> GetPersonnelQualificationStatisticsAsync();
/// <summary>
/// 获取即将过期的资质列表
/// </summary>
/// <param name="today"></param>
/// <param name="personnelIds"></param>
/// <param name="includeExpired"></param>
/// <returns></returns>
Task<List<ExpiringQualificationItemOutput>> GetExpiringAsync(DateTime today, IList<long> personnelIds = null, bool includeExpired = false);
}

View File

@ -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; }
}
}

View File

@ -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
/// <param name="userId">用户ID</param>
/// <returns></returns>
Task<List<WorkOrderGetOutput>> GetByUserIdAsync(long userId);
/// <summary>
/// 转换任务班次的任务时间
/// </summary>
/// <param name="workOrderEntity"></param>
/// <returns></returns>
Task<WorkOrderEntity> ConvertWorkOrderShift(WorkOrderEntity workOrderEntity);
Task<Dictionary<long, int>> GetPendingSubmitCountByCreatorAsync();
}

View File

@ -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;
/// <summary>
/// 工作台服务接口
/// 提供工作台相关的数据查询和业务逻辑处理
/// </summary>
public interface IWorkbenchService
{
/// <summary>
/// 获取用户本周日历数据
/// 包含任务排班信息和班次不可用性信息,用于日历视图展示
/// </summary>
/// <param name="input">查询参数</param>
/// <returns>本周日历数据</returns>
Task<WeeklyCalendarOutput> GetWeeklyCalendarAsync(WeeklyCalendarInput input);
}

View File

@ -0,0 +1,31 @@
using System;
namespace NPP.SmartSchedue.Api.Contracts.Services.Workbench.Input;
/// <summary>
/// 获取用户本周日历数据输入参数
/// </summary>
public class WeeklyCalendarInput
{
/// <summary>
/// 指定周的日期(可选,默认为当前周)
/// 可以是该周内任意一天,系统会自动计算周的开始和结束日期
/// </summary>
public DateTime? WeekDate { get; set; }
/// <summary>
/// 指定人员ID可选默认为当前登录用户
/// 如果指定则获取该人员的日历数据,否则获取当前登录用户的数据
/// </summary>
public long? PersonnelId { get; set; }
/// <summary>
/// 是否包含已完成的任务默认true
/// </summary>
public bool IncludeCompletedTasks { get; set; } = true;
/// <summary>
/// 是否包含不可用时间段默认true
/// </summary>
public bool IncludeUnavailableSlots { get; set; } = true;
}

View File

@ -0,0 +1,199 @@
using System;
using System.Collections.Generic;
namespace NPP.SmartSchedue.Api.Contracts.Services.Workbench.Output;
/// <summary>
/// 用户本周日历数据输出
/// </summary>
public class WeeklyCalendarOutput
{
/// <summary>
/// 周开始日期(周一)
/// </summary>
public DateTime WeekStartDate { get; set; }
/// <summary>
/// 周结束日期(周日)
/// </summary>
public DateTime WeekEndDate { get; set; }
/// <summary>
/// 人员ID
/// </summary>
public long PersonnelId { get; set; }
/// <summary>
/// 人员姓名
/// </summary>
public string PersonnelName { get; set; }
/// <summary>
/// 每日日历数据
/// </summary>
public List<CalendarDayItem> Days { get; set; } = new List<CalendarDayItem>();
}
/// <summary>
/// 单日日历数据项
/// </summary>
public class CalendarDayItem
{
/// <summary>
/// 日期
/// </summary>
public DateTime Date { get; set; }
/// <summary>
/// 星期几1=周一7=周日)
/// </summary>
public int DayOfWeek { get; set; }
/// <summary>
/// 星期几中文名称
/// </summary>
public string DayOfWeekName { get; set; }
/// <summary>
/// 该日的班次数据
/// </summary>
public List<CalendarShiftItem> Shifts { get; set; } = new List<CalendarShiftItem>();
}
/// <summary>
/// 班次日历数据项
/// </summary>
public class CalendarShiftItem
{
/// <summary>
/// 班次ID
/// </summary>
public long ShiftId { get; set; }
/// <summary>
/// 班次名称
/// </summary>
public string ShiftName { get; set; }
/// <summary>
/// 班次开始时间
/// </summary>
public TimeSpan StartTime { get; set; }
/// <summary>
/// 班次结束时间
/// </summary>
public TimeSpan EndTime { get; set; }
/// <summary>
/// 该班次的工作任务
/// </summary>
public List<CalendarTaskItem> Tasks { get; set; } = new List<CalendarTaskItem>();
/// <summary>
/// 该班次的不可用时间段
/// </summary>
public List<CalendarUnavailableItem> UnavailableItems { get; set; } = new List<CalendarUnavailableItem>();
}
/// <summary>
/// 任务日历项
/// </summary>
public class CalendarTaskItem
{
/// <summary>
/// 任务ID
/// </summary>
public long TaskId { get; set; }
/// <summary>
/// 任务代码
/// </summary>
public string TaskCode { get; set; }
/// <summary>
/// 项目号
/// </summary>
public string ProjectNumber { get; set; }
/// <summary>
/// 工序名称
/// </summary>
public string ProcessName { get; set; }
/// <summary>
/// 任务状态
/// </summary>
public int Status { get; set; }
/// <summary>
/// 状态名称
/// </summary>
public string StatusName { get; set; }
/// <summary>
/// 优先级
/// </summary>
public int Priority { get; set; }
/// <summary>
/// 预计工时
/// </summary>
public decimal? EstimatedHours { get; set; }
/// <summary>
/// 计划开始时间
/// </summary>
public DateTime PlannedStartTime { get; set; }
/// <summary>
/// 计划结束时间
/// </summary>
public DateTime PlannedEndTime { get; set; }
/// <summary>
/// 备注
/// </summary>
public string Remarks { get; set; }
}
/// <summary>
/// 不可用时间段项
/// </summary>
public class CalendarUnavailableItem
{
/// <summary>
/// 不可用记录ID
/// </summary>
public long UnavailabilityId { get; set; }
/// <summary>
/// 不可用原因类型
/// </summary>
public int ReasonType { get; set; }
/// <summary>
/// 原因类型名称
/// </summary>
public string ReasonTypeName { get; set; }
/// <summary>
/// 备注说明
/// </summary>
public string Remark { get; set; }
/// <summary>
/// 生效开始时间(可选)
/// </summary>
public TimeSpan? EffectiveStartTime { get; set; }
/// <summary>
/// 生效结束时间(可选)
/// </summary>
public TimeSpan? EffectiveEndTime { get; set; }
/// <summary>
/// 优先级权重
/// </summary>
public int Priority { get; set; }
}

View File

@ -18,5 +18,29 @@ public static partial class CacheKeys
/// <param name="id">模块Id</param>
/// <returns></returns>
public static string GetModuleActionKey(long id) => $"{ModuleActionKey}{id}";
/// <summary>
/// 通知设置缓存
/// </summary>
[Description("通知设置缓存")]
public const string NotificationSettingsKey = "notification:settings:list:";
/// <summary>
/// DynamicDepartmentIds
/// </summary>
[Description("动态部门人员数据")] public const string DynamicDepartment = "dynamic:department:list:";
/// <summary>
/// 获取动态部门人员数据
/// </summary>
/// <param name="id"></param>
/// <returns></returns>
public static string GetDynamicDepartmentKey(long id) => $"{DynamicDepartment}{id}";
/// <summary>
/// 人员缓存
/// </summary>
[Description("人员缓存")]
public const string UserListKey = "user:list:all:";
}

View File

@ -25,7 +25,6 @@
<ItemGroup>
<Folder Include="Repositories\" />
<Folder Include="Services\" />
</ItemGroup>
<ItemGroup>
@ -34,11 +33,12 @@
<ItemGroup>
<PackageReference Include="Google.OrTools" Version="9.14.6206" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="9.0.5" />
<PackageReference Include="Microsoft.Extensions.Http.Polly" Version="9.0.6" />
<PackageReference Include="Polly" Version="8.4.2" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\admin\ZhonTai.Admin.Contracts\ZhonTai.Admin.Contracts.csproj" />
<ProjectReference Include="..\NPP.SmartSchedue.Api.Contracts\NPP.SmartSchedue.Api.Contracts.csproj" />
</ItemGroup>
</Project>

View File

@ -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<NotificationHisto
{
if (!updates.Any()) return;
// 使用事务批量更新
await Orm.Transaction(async () =>
// 使用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);
}
}
}

View File

@ -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<NotificationSetti
/// </summary>
public async Task<List<NotificationSettingEntity>> 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<NotificationSettingEntity>();
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;
}
}

View File

@ -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;

View File

@ -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<IQualificationRepository, QualificationRepository>();
services.AddScoped<IPersonnelQualificationRepository, PersonnelQualificationRepository>();
return services;
}
@ -62,5 +67,38 @@ namespace NPP.SmartSchedue.Api
return services;
}
/// <summary>
/// 注册通知相关服务
/// </summary>
public static IServiceCollection AddNotificationServices(this IServiceCollection services)
{
// 注册通知相关Repository
services.AddScoped<INotificationSettingRepository, NotificationSettingRepository>();
services.AddScoped<IPersonnelGroupRepository, PersonnelGroupRepository>();
services.AddScoped<INotificationHistoryRepository, NotificationHistoryRepository>();
// 注册通知相关Service
services.AddScoped<INotificationService, NotificationService>();
services.AddScoped<IEmailNotificationService, EmailNotificationService>();
services.AddScoped<ISystemMessageService, SystemMessageService>();
services.AddScoped<INotificationTemplateService, NotificationTemplateService>();
return services;
}
/// <summary>
/// 注册工作订单整合操作相关服务
/// </summary>
public static IServiceCollection AddWorkOrderIntegrationOperationServices(this IServiceCollection services)
{
// 注册状态验证器
services.AddScoped<WorkOrderStatusValidator>();
// 注册统一操作服务接口和实现
services.AddScoped<IWorkOrderIntegrationOperationService, WorkOrderIntegrationOperationService>();
return services;
}
}
}

View File

@ -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<long>();
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;

View File

@ -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);
}
/// <summary>
@ -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业务约束...");
// 业务约束1FL优先级规则 - 对应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);
}
/// <summary>
@ -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);
// }
}
/// <summary>
@ -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);
// }
}
/// <summary>
@ -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)

View File

@ -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<IntegrationRecordService> _logger;
public IntegrationRecordService(
IIntegrationRecordRepository integrationRecordRepository,
WorkOrderRepository workOrderRepository,
EquipmentRepository equipmentRepository,
ILogger<IntegrationRecordService> logger)
{
_integrationRecordRepository = integrationRecordRepository;
_workOrderRepository = workOrderRepository;
_equipmentRepository = equipmentRepository;
_logger = logger;
}
@ -922,6 +926,112 @@ namespace NPP.SmartSchedue.Api.Services.Integration
#endregion
#region
/// <summary>
/// 修改整合记录中的任务分配
/// 根据选中的人员修改任务的指定人员ID和姓名并更新整合记录
/// </summary>
[HttpPost]
public async Task<bool> 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
/// <summary>
@ -929,10 +1039,10 @@ namespace NPP.SmartSchedue.Api.Services.Integration
/// 深度业务思考:为未来的通知系统集成预留完整的接口结构
/// </summary>
[HttpPost]
public async Task<NotificationSendResult> SendPublishNotificationAsync(long recordId, string notificationType, List<long> targetPersonnelIds, string customMessage = "")
public async Task<IntegrationNotificationResult> SendPublishNotificationAsync(long recordId, string notificationType, List<long> 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
/// <summary>
/// 执行任务发布
/// 业务思考:
/// 1. 批量处理任务发布,但每个任务独立处理避免相互影响
/// 2. 详细记录发布过程和结果,便于问题排查和审计
/// 3. 支持部分成功的发布场景,返回成功和失败的详细信息
/// 4. 在发布失败时提供具体的失败原因和建议
/// </summary>
private async Task<(List<PublishedTaskDetail> PublishedTasks, List<FailedPublishTaskDetail> FailedPublishTasks)> ExecuteTaskPublishAsync(
List<WorkOrderEntity> tasks,
@ -1065,18 +1180,29 @@ namespace NPP.SmartSchedue.Api.Services.Integration
var publishedTasks = new List<PublishedTaskDetail>();
var failedTasks = new List<FailedPublishTaskDetail>();
_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<string>();
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<string> { "发布过程中遇到业务规则限制" }
DetailedErrors = failureReasons.Any() ? failureReasons : new List<string> { "发布过程中遇到未知问题" }
});
_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<string> { ex.Message }
DetailedErrors = new List<string> { 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);
}
/// <summary>
/// 发布单个任务
/// 深度业务思考:
/// 1. 验证任务状态转换的合法性只有PendingIntegration状态的任务可以发布
/// 2. 支持AutoStartTasksAfterPublish选项发布后可直接开始任务
/// 3. 完整的异常处理和日志记录
/// 4. 原子性操作确保数据一致性
/// </summary>
private async Task<bool> 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
/// <summary>
/// 发送发布通知
/// </summary>
private async Task<List<NotificationSendResult>> SendPublishNotificationsAsync(IntegrationRecordEntity integrationRecord, PublishIntegrationRecordInput input)
private async Task<List<IntegrationNotificationResult>> SendPublishNotificationsAsync(IntegrationRecordEntity integrationRecord, PublishIntegrationRecordInput input)
{
var results = new List<NotificationSendResult>();
var results = new List<IntegrationNotificationResult>();
// 预留:根据发布规则确定通知对象和内容
var targetPersonnelIds = new List<long>(); // 从业务规则中获取

View File

@ -1057,6 +1057,17 @@ namespace NPP.SmartSchedue.Api.Services.Integration
return await _integrationRecordService.PublishIntegrationRecordAsync(input);
}
/// <summary>
/// 修改整合记录中的任务分配
/// 根据选中的人员修改任务的指定人员ID和姓名并更新整合记录
/// </summary>
/// <param name="input">任务修改输入参数</param>
/// <returns>修改结果</returns>
[HttpPost]
public async Task<bool> ModifyIntegrationRecordTaskAsync(IntegrationRecordTaskModifyInput input)
{
return await _integrationRecordService.ModifyIntegrationRecordTaskAsync(input);
}
/// <summary>
/// 转换全局分配结果为人员分配结果

File diff suppressed because it is too large Load Diff

View File

@ -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
{
/// <summary>
/// 工作订单整合操作统一服务实现
/// </summary>
[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<WorkOrderIntegrationOperationService> _logger;
private readonly NotificationScheduledService _notificationScheduledService;
public WorkOrderIntegrationOperationService(
IWorkOrderService workOrderService,
IIntegrationRecordService integrationRecordService,
WorkOrderRepository workOrderRepository,
IntegrationRecordRepository integrationRecordRepository,
WorkOrderStatusValidator statusValidator,
ILogger<WorkOrderIntegrationOperationService> logger,
NotificationScheduledService notificationScheduledService)
{
_workOrderService = workOrderService;
_integrationRecordService = integrationRecordService;
_workOrderRepository = workOrderRepository;
_integrationRecordRepository = integrationRecordRepository;
_statusValidator = statusValidator;
_logger = logger;
_notificationScheduledService = notificationScheduledService;
}
/// <summary>
/// 修改任务统一入口
/// </summary>
[HttpPost]
public async Task<WorkOrderOperationResult> 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<long>();
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}");
}
}
/// <summary>
/// 删除任务统一入口
/// </summary>
[HttpPost]
public async Task<WorkOrderOperationResult> 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<long>();
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}");
}
}
/// <summary>
/// 取消任务统一入口
/// </summary>
[HttpPost]
public async Task<WorkOrderOperationResult> 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<long>();
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
/// <summary>
/// 判断任务修改是否需要触发重新分配
/// 基于业务关键字段分析,确保分配策略的准确性
/// </summary>
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;
}
/// <summary>
/// 修改任务后更新整合记录(仅对已分配任务生效)
/// </summary>
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<string>();
// 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;
}
}
/// <summary>
/// 删除任务后更新整合记录(仅对已分配任务生效)
/// </summary>
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);
}
}
/// <summary>
/// 取消任务后更新整合记录(仅对已分配任务生效)
/// </summary>
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);
}
}
/// <summary>
/// 创建失败结果
/// </summary>
private WorkOrderOperationResult CreateFailResult(string message)
{
return new WorkOrderOperationResult
{
IsSuccess = false,
Message = message,
ErrorDetails = { message }
};
}
/// <summary>
/// 验证修改操作参数
/// </summary>
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();
}
/// <summary>
/// 验证删除操作参数
/// </summary>
private WorkOrderValidationResult ValidateDeleteParameters(WorkOrderDeleteOperationInput input)
{
if (input == null)
return WorkOrderValidationResult.Fail("输入参数不能为空");
if (input.TaskId <= 0)
return WorkOrderValidationResult.Fail("任务ID必须大于0");
return WorkOrderValidationResult.Success();
}
/// <summary>
/// 验证取消操作参数
/// </summary>
private WorkOrderValidationResult ValidateCancelParameters(WorkOrderCancelOperationInput input)
{
if (input == null)
return WorkOrderValidationResult.Fail("输入参数不能为空");
if (input.TaskId <= 0)
return WorkOrderValidationResult.Fail("任务ID必须大于0");
return WorkOrderValidationResult.Success();
}
/// <summary>
/// 验证修改操作的业务规则
/// </summary>
private Task<WorkOrderValidationResult> ValidateModifyBusinessRulesAsync(WorkOrderModifyOperationInput input)
{
// 这里可以添加具体的业务规则验证
// 例如:检查任务是否与其他任务有依赖关系等
return Task.FromResult(WorkOrderValidationResult.Success());
}
/// <summary>
/// 验证删除操作的业务规则
/// </summary>
private Task<WorkOrderValidationResult> ValidateDeleteBusinessRulesAsync(WorkOrderDeleteOperationInput input)
{
// 这里可以添加具体的业务规则验证
// 例如:检查任务是否与其他任务有依赖关系等
return Task.FromResult(WorkOrderValidationResult.Success());
}
/// <summary>
/// 验证取消操作的业务规则
/// </summary>
private Task<WorkOrderValidationResult> ValidateCancelBusinessRulesAsync(WorkOrderCancelOperationInput input)
{
// 这里可以添加具体的业务规则验证
// 例如:检查任务是否与其他任务有依赖关系等
return Task.FromResult(WorkOrderValidationResult.Success());
}
/// <summary>
/// 任务删除后更新人员分配结果
/// </summary>
private async Task UpdatePersonnelAllocationResultAfterTaskRemoval(IntegrationRecordEntity record, long taskId)
{
try
{
if (string.IsNullOrWhiteSpace(record.PersonnelAllocationResultJson) || record.PersonnelAllocationResultJson == "{}")
return;
// 解析人员分配结果
var allocationResult = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, object>>(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<object>();
foreach (var allocation in successfulAllocations.Value.EnumerateArray())
{
if (allocation.TryGetProperty("TaskId", out var taskIdElement) &&
taskIdElement.GetInt64() != taskId)
{
updatedAllocations.Add(System.Text.Json.JsonSerializer.Deserialize<object>(allocation.GetRawText()));
}
}
allocationResult["successfulAllocations"] = updatedAllocations;
}
}
// 添加到失败列表
if (!allocationResult.ContainsKey("failedAllocations"))
allocationResult["failedAllocations"] = new List<object>();
var failedList = allocationResult["failedAllocations"] as List<object> ?? new List<object>();
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);
}
}
/// <summary>
/// 任务删除后更新设备分配结果
/// </summary>
private async Task UpdateEquipmentAllocationResultAfterTaskRemoval(IntegrationRecordEntity record, long taskId)
{
try
{
if (string.IsNullOrWhiteSpace(record.EquipmentAllocationResultJson) || record.EquipmentAllocationResultJson == "{}")
return;
// 解析设备分配结果
var allocationResult = System.Text.Json.JsonSerializer.Deserialize<Dictionary<string, object>>(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<object>();
foreach (var allocation in successfulAllocations.Value.EnumerateArray())
{
if (allocation.TryGetProperty("TaskId", out var taskIdElement) &&
taskIdElement.GetInt64() != taskId)
{
updatedAllocations.Add(System.Text.Json.JsonSerializer.Deserialize<object>(allocation.GetRawText()));
}
}
allocationResult["successfulAllocations"] = updatedAllocations;
}
}
// 添加到失败列表
if (!allocationResult.ContainsKey("failedAllocations"))
allocationResult["failedAllocations"] = new List<object>();
var failedList = allocationResult["failedAllocations"] as List<object> ?? new List<object>();
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);
}
}
/// <summary>
/// 任务取消后更新人员分配结果
/// </summary>
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<Dictionary<string, object>>(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<object>();
foreach (var allocation in successfulAllocations.Value.EnumerateArray())
{
if (allocation.TryGetProperty("TaskId", out var taskIdElement) &&
taskIdElement.GetInt64() != taskId)
{
updatedAllocations.Add(System.Text.Json.JsonSerializer.Deserialize<object>(allocation.GetRawText()));
}
}
allocationResult["successfulAllocations"] = updatedAllocations;
}
}
// 添加到失败列表
if (!allocationResult.ContainsKey("failedAllocations"))
allocationResult["failedAllocations"] = new List<object>();
var failedList = allocationResult["failedAllocations"] as List<object> ?? new List<object>();
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);
}
}
/// <summary>
/// 任务取消后更新设备分配结果
/// </summary>
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<Dictionary<string, object>>(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<object>();
foreach (var allocation in successfulAllocations.Value.EnumerateArray())
{
if (allocation.TryGetProperty("TaskId", out var taskIdElement) &&
taskIdElement.GetInt64() != taskId)
{
updatedAllocations.Add(System.Text.Json.JsonSerializer.Deserialize<object>(allocation.GetRawText()));
}
}
allocationResult["successfulAllocations"] = updatedAllocations;
}
}
// 添加到失败列表
if (!allocationResult.ContainsKey("failedAllocations"))
allocationResult["failedAllocations"] = new List<object>();
var failedList = allocationResult["failedAllocations"] as List<object> ?? new List<object>();
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);
}
}
/// <summary>
/// 计算变更影响评分
/// </summary>
private int CalculateChangeImpact(List<string> 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);
}
/// <summary>
/// 日期变更时更新分配结果
/// </summary>
private async Task UpdateAllocationResultsForDateChange(IntegrationRecordEntity record, long taskId, WorkOrderEntity originalTask, WorkOrderUpdateInput modifyData)
{
// 日期变更可能需要重新评估所有分配结果
// 这里可以实现具体的日期变更处理逻辑
_logger.LogInformation("任务{TaskId}日期变更,正在更新分配结果", taskId);
await Task.CompletedTask; // 占位实现
}
/// <summary>
/// 重新计算人员公平性评分
/// </summary>
private async Task RecalculatePersonnelFairnessScore(IntegrationRecordEntity record)
{
// 这里可以实现人员公平性评分的重新计算逻辑
// 暂时保持原有分数
_logger.LogInformation("正在重新计算人员公平性评分整合记录ID: {IntegrationRecordId}", record.Id);
await Task.CompletedTask; // 占位实现
}
/// <summary>
/// 重新计算设备利用率
/// </summary>
private async Task RecalculateEquipmentUtilizationRate(IntegrationRecordEntity record)
{
// 这里可以实现设备利用率的重新计算逻辑
// 暂时保持原有数值
_logger.LogInformation("正在重新计算设备利用率整合记录ID: {IntegrationRecordId}", record.Id);
await Task.CompletedTask; // 占位实现
}
/// <summary>
/// 工序班次变更时更新策略配置
/// </summary>
private async Task UpdateStrategyConfigForProcessShiftChange(IntegrationRecordEntity record, WorkOrderEntity originalTask, WorkOrderUpdateInput modifyData)
{
// 工序班次变更可能需要更新分配策略
_logger.LogInformation("工序班次变更正在更新策略配置整合记录ID: {IntegrationRecordId}", record.Id);
await Task.CompletedTask; // 占位实现
}
/// <summary>
/// 重新评估所有分配结果
/// </summary>
private async Task ReEvaluateAllAllocationResults(IntegrationRecordEntity record, long taskId)
{
// 重新评估所有分配结果的逻辑
_logger.LogInformation("正在重新评估所有分配结果任务ID: {TaskId}整合记录ID: {IntegrationRecordId}", taskId, record.Id);
await Task.CompletedTask; // 占位实现
}
#endregion
}
}

View File

@ -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
{
/// <summary>
/// 任务状态验证器
/// 专门负责步骤3状态验证的详细分析
/// </summary>
public class WorkOrderStatusValidator
{
private readonly WorkOrderRepository _workOrderRepository;
private readonly ILogger<WorkOrderStatusValidator> _logger;
public WorkOrderStatusValidator(
WorkOrderRepository workOrderRepository,
ILogger<WorkOrderStatusValidator> logger)
{
_workOrderRepository = workOrderRepository;
_logger = logger;
}
/// <summary>
/// 修改操作状态验证(基于已查询的任务对象)
/// </summary>
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}");
}
}
/// <summary>
/// 删除操作状态验证(基于已查询的任务对象)
/// </summary>
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}");
}
}
/// <summary>
/// 取消操作状态验证(基于已查询的任务对象)
/// </summary>
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}");
}
}
}
}

View File

@ -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;
/// <summary>
/// 邮件通知服务实现
/// 决策点1基础通知方式 - 邮件通知
/// 通知服务 邮件
/// </summary>
public class EmailNotificationService : IEmailNotificationService
[DynamicApi(Area = "app")]
public class EmailNotificationService : BaseService, IEmailNotificationService, IDynamicApi
{
private readonly ILogger<EmailNotificationService> _logger;
private readonly IConfiguration _configuration;
private readonly EmailConfiguration _emailConfig;
private readonly INotificationTemplateService _templateService;
// 邮件配置节点名称
private const string EmailConfigSection = "EmailNotification";
public EmailNotificationService(
ILogger<EmailNotificationService> logger,
IConfiguration configuration,
IOptions<EmailConfiguration> 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
/// <summary>
/// 发送邮件通知
/// </summary>
public async Task<bool> 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;
}
/// <summary>
/// 发送邮件通知(带附件)
/// 发送单个邮件
/// </summary>
public async Task<bool> SendEmailWithAttachmentsAsync(
string recipientEmail,
string subject,
string content,
List<string> attachments,
bool isHtml = true)
[HttpPost]
public async Task<SendEmailOutput> 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
/// <summary>
/// 批量发送邮件通知
/// 批量发送邮件
/// </summary>
public async Task<Dictionary<string, bool>> BatchSendEmailAsync(
List<string> recipients,
string subject,
string content,
bool isHtml = true)
[HttpPost]
public async Task<BatchSendEmailOutput> BatchSendEmailAsync(BatchSendEmailInput input)
{
var results = new Dictionary<string, bool>();
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;
}
/// <summary>
/// 个性化批量发送邮件通知
/// 个性化批量发送邮件
/// </summary>
public async Task<Dictionary<string, bool>> BatchSendPersonalizedEmailAsync(List<EmailItem> emailItems)
[HttpPost]
public async Task<BatchSendEmailOutput> BatchSendPersonalizedEmailAsync(BatchSendPersonalizedEmailInput input)
{
var results = new Dictionary<string, bool>();
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
/// <summary>
/// 使用模板发送邮件
/// </summary>
public async Task<bool> SendEmailByTemplateAsync(
string recipientEmail,
string subjectTemplate,
string contentTemplate,
Dictionary<string, string> variables,
bool isHtml = true)
[HttpPost]
public async Task<SendEmailOutput> 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<string, string>()
};
var subjectResult = await _templateService.RenderTemplateAsync(subjectInput);
var contentInput = new RenderTemplateInput
{
Template = input.ContentTemplate,
Variables = input.Variables ?? new Dictionary<string, string>()
};
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
/// <summary>
/// 验证邮箱地址格式
/// </summary>
public bool IsValidEmail(string email)
public async Task<bool> IsValidEmailAsync(string email)
{
return await Task.FromResult(IsValidEmail(email));
}
/// <summary>
/// 检查邮件服务器连接状态
/// </summary>
public async Task<EmailServerStatusOutput> 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
/// <summary>
/// 验证邮箱地址格式
/// </summary>
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
}
}
/// <summary>
/// 检查邮件服务器连接状态
/// </summary>
public async Task<bool> 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
/// <summary>
/// 获取邮件配置
/// </summary>
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;
}
}
/// <summary>
/// 创建SMTP客户端
/// </summary>
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;
}
/// <summary>
/// 创建邮件消息
/// </summary>
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
/// <summary>
/// 添加附件
/// </summary>
private void AddAttachments(MailMessage mailMessage, List<string> attachments)
{
foreach (var attachmentPath in attachments)
{
if (File.Exists(attachmentPath))
{
var attachment = new Attachment(attachmentPath);
mailMessage.Attachments.Add(attachment);
}
else
{
_logger.LogWarning("附件文件不存在:{Path}", attachmentPath);
}
}
}
/// <summary>
/// 邮件配置类
/// 创建失败的批量发送输出
/// </summary>
private class EmailConfiguration
private BatchSendEmailOutput CreateFailedBatchOutput(IEnumerable<string> 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
}

View File

@ -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;
/// <summary>
/// 通知服务 定时消息
/// </summary>
[DynamicApi(Area = "app")]
public class NotificationScheduledService : BaseService, IDynamicApi
{
private readonly ILogger<NotificationScheduledService> _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<NotificationScheduledService> logger,
PersonnelQualificationService personnelQualificationService)
{
_logger = logger;
_workOrderService = workOrderService;
_userService = userService;
_notificationService = notificationService;
_personnelQualificationService = personnelQualificationService;
}
/// <summary>
/// 待提交任务提醒
/// </summary>
/// <returns></returns>
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> 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<SendNotificationInput>();
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<string, string>
{
["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<long> { 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<long> { 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 });
}
/// <summary>
/// 下周计划与请假提醒
/// </summary>
/// <returns></returns>
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> 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<string, string>
{
["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<long>();
var emailTargets = new List<long>();
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<SendNotificationInput>();
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 });
}
/// <summary>
/// 任务分配提醒(周五提醒排班管理员进行排班)
/// </summary>
/// <returns></returns>
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> 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<string, string>
{
["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<long>();
var emailTargets = new List<long>();
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<SendNotificationInput>();
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 });
}
/// <summary>
/// 人员资质到期预警
/// </summary>
/// <returns></returns>
[HttpGet]
[AllowAnonymous]
public async Task<IActionResult> 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<SendNotificationInput>();
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<string, string>
{
["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<long> { 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<long> { 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 });
}
/// <summary>
/// 统一对外:工作任务操作通知(供 WorkOrderIntegrationOperationService 调用)
/// </summary>
/// <param name="operationType">Modify/Delete/Cancel</param>
/// <param name="taskId">任务ID</param>
/// <param name="operatorUserId">操作者用户ID可空</param>
/// <param name="operatorName">操作者名称(可空)</param>
/// <param name="remarks">备注(可空)</param>
/// <returns></returns>
[HttpPost]
[AllowAnonymous]
public async Task<IActionResult> SendWorkOrderOperationNotificationAsync(
string operationType,
long taskId,
long? operatorUserId = null,
string operatorName = null,
string remarks = null,
IList<long> designatedPersonnelIds = null,
IList<string> 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<string, string>
{
["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<long>();
var emailTargets = new List<long>();
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<SendNotificationInput>();
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<string> Render(string template, Dictionary<string, string> 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<DateTime?> GetLastSentAt(long settingId, long personnelId, string businessType)
{
try
{
var histories = await _notificationService.GetNotificationHistoryPageAsync(new ZhonTai.Admin.Core.Dto.PageInput<NotificationHistoryPageInput>
{
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<NotificationSettingOutput> GetNotificationSettingCache(string key)
{
var settings = await _notificationService.GetNotificationSettingListAsync(true);
return settings.FirstOrDefault(m => m.NotificationName == key);
}
private async Task<List<long>> GetNotificationPersonListAsync(NotificationSettingOutput? setting = null,
List<long>? otherPersons = null)
{
var notificationPersonList = new List<long>();
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;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -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;
/// <summary>
/// 通知模板服务实现
/// 决策点7模板通知支持通知内容模板可替换变量
/// 使用简单的变量替换机制:{变量名}
/// 通知服务 模板
/// </summary>
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
/// <summary>
/// 渲染通知模板
/// 支持变量替换,如:{变量名}
/// </summary>
public async Task<string> RenderTemplateAsync(string template, Dictionary<string, string> variables)
/// <param name="input">渲染模板输入参数</param>
/// <returns>渲染结果</returns>
[HttpPost]
public async Task<RenderTemplateOutput> 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
/// <summary>
/// 验证模板语法是否正确
/// </summary>
/// <param name="input">验证模板输入参数</param>
/// <returns>验证结果</returns>
[HttpPost]
public async Task<ValidateTemplateOutput> 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
/// <summary>
/// 提取模板中的变量列表
/// </summary>
/// <param name="input">提取变量输入参数</param>
/// <returns>变量列表</returns>
[HttpPost]
public async Task<ExtractVariablesOutput> 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
/// <summary>
/// 获取系统内置变量
/// </summary>
/// <returns>系统变量字典</returns>
[HttpPost]
public async Task<GetSystemVariablesOutput> 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}"
};
}
}
/// <summary>
/// 同步渲染通知模板
/// 获取业务相关变量
/// </summary>
public string RenderTemplate(string template, Dictionary<string, string> variables)
/// <param name="input">获取业务变量输入参数</param>
/// <returns>业务变量字典</returns>
[HttpPost]
public async Task<GetBusinessVariablesOutput> 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
/// <summary>
/// 获取预定义模板列表
/// </summary>
/// <param name="input">获取模板列表输入参数</param>
/// <returns>预定义模板列表</returns>
[HttpPost]
public async Task<GetPredefinedTemplatesOutput> 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}"
};
}
}
/// <summary>
/// 获取指定预定义模板
/// </summary>
/// <param name="input">获取指定模板输入参数</param>
/// <returns>预定义模板</returns>
[HttpPost]
public async Task<GetPredefinedTemplateOutput> 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
/// <summary>
/// 核心模板渲染逻辑
/// </summary>
/// <param name="template">模板内容</param>
/// <param name="variables">变量字典</param>
/// <returns>渲染后的内容</returns>
private string RenderTemplateCore(string template, Dictionary<string, string> variables)
{
if (string.IsNullOrWhiteSpace(template))
return string.Empty;
@ -59,22 +285,12 @@ public class NotificationTemplateService : INotificationTemplateService
return result;
}
#endregion
#region
/// <summary>
/// 验证模板语法
/// 核心模板验证逻辑
/// </summary>
public async Task<TemplateValidationResult> ValidateTemplateAsync(string template)
{
return await Task.FromResult(ValidateTemplate(template));
}
/// <summary>
/// 同步验证模板语法
/// </summary>
public TemplateValidationResult ValidateTemplate(string template)
/// <param name="template">模板内容</param>
/// <returns>验证结果</returns>
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
/// <summary>
/// 提取模板中的变量列表
/// 核心变量提取逻辑
/// </summary>
public async Task<List<string>> ExtractVariablesAsync(string template)
{
return await Task.FromResult(ExtractVariables(template));
}
/// <summary>
/// 同步提取模板中的变量列表
/// </summary>
public List<string> ExtractVariables(string template)
/// <param name="template">模板内容</param>
/// <returns>变量名列表</returns>
private List<string> ExtractVariablesCore(string template)
{
if (string.IsNullOrWhiteSpace(template))
return new List<string>();
@ -159,18 +365,15 @@ public class NotificationTemplateService : INotificationTemplateService
return variables.OrderBy(v => v).ToList();
}
#endregion
#region
/// <summary>
/// 获取系统内置变量
/// 获取系统内置变量核心逻辑
/// </summary>
public async Task<Dictionary<string, string>> GetSystemVariablesAsync()
/// <returns>系统变量字典</returns>
private Dictionary<string, string> GetSystemVariablesCore()
{
var now = DateTime.Now;
var systemVariables = new Dictionary<string, string>
return new Dictionary<string, string>
{
["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);
}
/// <summary>
/// 获取业务相关变量
/// 获取业务相关变量核心逻辑
/// </summary>
public async Task<Dictionary<string, string>> GetBusinessVariablesAsync(
string businessType,
long? businessId = null,
string businessData = "")
/// <param name="businessType">业务类型</param>
/// <param name="businessId">业务ID</param>
/// <param name="businessData">业务数据</param>
/// <returns>业务变量字典</returns>
private async Task<Dictionary<string, string>> GetBusinessVariablesCore(
string businessType,
long? businessId,
string businessData)
{
var businessVariables = new Dictionary<string, string>
{
@ -226,43 +431,13 @@ public class NotificationTemplateService : INotificationTemplateService
#endregion
#region
/// <summary>
/// 获取预定义模板列表
/// </summary>
public async Task<List<PredefinedTemplate>> 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);
}
/// <summary>
/// 获取指定预定义模板
/// </summary>
public async Task<PredefinedTemplate> 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
/// <summary>
/// 验证变量名是否符合规范
/// </summary>
/// <param name="variableName">变量名</param>
/// <returns>是否符合规范</returns>
private static bool IsValidVariableName(string variableName)
{
if (string.IsNullOrWhiteSpace(variableName))
@ -275,6 +450,8 @@ public class NotificationTemplateService : INotificationTemplateService
/// <summary>
/// 获取星期几的中文名称
/// </summary>
/// <param name="dayOfWeek">星期枚举</param>
/// <returns>中文名称</returns>
private static string GetWeekdayName(DayOfWeek dayOfWeek)
{
return dayOfWeek switch
@ -293,6 +470,10 @@ public class NotificationTemplateService : INotificationTemplateService
/// <summary>
/// 添加工作任务相关变量
/// </summary>
/// <param name="variables">变量字典</param>
/// <param name="businessId">业务ID</param>
/// <param name="businessData">业务数据</param>
/// <returns>异步任务</returns>
private async Task AddWorkOrderVariablesAsync(Dictionary<string, string> variables, long? businessId, string businessData)
{
// 这里可以根据businessId查询工作任务详情然后添加相关变量
@ -324,6 +505,10 @@ public class NotificationTemplateService : INotificationTemplateService
/// <summary>
/// 添加设备相关变量
/// </summary>
/// <param name="variables">变量字典</param>
/// <param name="businessId">业务ID</param>
/// <param name="businessData">业务数据</param>
/// <returns>异步任务</returns>
private async Task AddEquipmentVariablesAsync(Dictionary<string, string> variables, long? businessId, string businessData)
{
variables["EquipmentId"] = businessId?.ToString() ?? "";
@ -334,6 +519,10 @@ public class NotificationTemplateService : INotificationTemplateService
/// <summary>
/// 添加人员相关变量
/// </summary>
/// <param name="variables">变量字典</param>
/// <param name="businessId">业务ID</param>
/// <param name="businessData">业务数据</param>
/// <returns>异步任务</returns>
private async Task AddPersonnelVariablesAsync(Dictionary<string, string> variables, long? businessId, string businessData)
{
variables["PersonnelId"] = businessId?.ToString() ?? "";
@ -344,6 +533,10 @@ public class NotificationTemplateService : INotificationTemplateService
/// <summary>
/// 添加维护相关变量
/// </summary>
/// <param name="variables">变量字典</param>
/// <param name="businessId">业务ID</param>
/// <param name="businessData">业务数据</param>
/// <returns>异步任务</returns>
private async Task AddMaintenanceVariablesAsync(Dictionary<string, string> variables, long? businessId, string businessData)
{
variables["MaintenanceId"] = businessId?.ToString() ?? "";
@ -354,6 +547,7 @@ public class NotificationTemplateService : INotificationTemplateService
/// <summary>
/// 获取默认预定义模板
/// </summary>
/// <returns>预定义模板列表</returns>
private static List<PredefinedTemplate> GetDefaultPredefinedTemplates()
{
return new List<PredefinedTemplate>

View File

@ -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;
/// <summary>
/// 系统消息服务实现
/// 决策点1基础通知方式 - 系统消息通知
/// 业务思考:系统消息是内部通知的核心方式,需要支持实时推送、状态管理、操作交互等功能
/// 设计原则:
/// 1. 消息持久化存储,确保不丢失
/// 2. 支持消息状态管理(未读/已读/已删除)
/// 3. 支持操作按钮,实现交互式消息
/// 4. 支持批量操作,提高效率
/// 5. 支持模板化消息,统一格式
/// 通知服务 系统消息
/// </summary>
public class SystemMessageService : ISystemMessageService
[DynamicApi(Area = "app")]
public class SystemMessageService : BaseService, ISystemMessageService, IDynamicApi
{
private readonly ILogger<SystemMessageService> _logger;
private readonly INotificationTemplateService _templateService;
private readonly IBaseRepository<NotificationHistoryEntity> _notificationHistoryRepository;
private readonly IUnitOfWorkManager _uowManager;
private readonly IRepositoryBase<NotificationHistoryEntity> _notificationHistoryRepository;
public SystemMessageService(
ILogger<SystemMessageService> logger,
INotificationTemplateService templateService,
IBaseRepository<NotificationHistoryEntity> notificationHistoryRepository,
IUnitOfWorkManager uowManager)
IRepositoryBase<NotificationHistoryEntity> 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
/// 发送系统消息
/// 业务场景:工作任务分配、设备状态变更、排班通知等
/// </summary>
public async Task<bool> SendSystemMessageAsync(
long recipientPersonnelId,
string title,
string content,
SystemMessageTypeEnum messageType = SystemMessageTypeEnum.Info,
string businessType = "",
long? businessId = null)
[HttpPost]
public async Task<SendSystemMessageOutput> 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
/// 发送带操作按钮的系统消息
/// 业务场景:任务确认、审批流程、操作确认等交互式场景
/// </summary>
public async Task<bool> SendSystemMessageWithActionsAsync(
long recipientPersonnelId,
string title,
string content,
List<SystemMessageAction> actions,
SystemMessageTypeEnum messageType = SystemMessageTypeEnum.Info,
string businessType = "",
long? businessId = null)
[HttpPost]
public async Task<SendSystemMessageOutput> 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
/// 批量发送系统消息
/// 业务场景:排班变更通知、紧急通知、系统维护通知等需要群发的场景
/// </summary>
public async Task<Dictionary<long, bool>> BatchSendSystemMessageAsync(
List<long> recipientPersonnelIds,
string title,
string content,
SystemMessageTypeEnum messageType = SystemMessageTypeEnum.Info,
string businessType = "",
long? businessId = null)
[HttpPost]
public async Task<BatchSendSystemMessageOutput> BatchSendSystemMessageAsync(BatchSendSystemMessageInput input)
{
var results = new Dictionary<long, bool>();
var results = new Dictionary<long, SendSystemMessageOutput>();
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<NotificationHistoryEntity>();
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);
}
/// <summary>
/// 个性化批量发送系统消息
/// 业务场景:个性化任务分配通知、个性化提醒等需要不同内容的场景
/// </summary>
public async Task<Dictionary<long, bool>> BatchSendPersonalizedSystemMessageAsync(List<SystemMessageItem> messageItems)
[HttpPost]
public async Task<BatchSendSystemMessageOutput> BatchSendPersonalizedSystemMessageAsync(List<SystemMessageItem> messageItems)
{
var results = new Dictionary<long, bool>();
var results = new Dictionary<long, SendSystemMessageOutput>();
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<NotificationHistoryEntity>();
var currentTime = DateTime.Now;
var successfulItems = new List<SystemMessageItem>();
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
/// 使用模板发送系统消息
/// 业务场景:标准化通知,如任务分配模板、维护提醒模板等
/// </summary>
public async Task<bool> SendSystemMessageByTemplateAsync(
long recipientPersonnelId,
string titleTemplate,
string contentTemplate,
Dictionary<string, string> variables,
SystemMessageTypeEnum messageType = SystemMessageTypeEnum.Info,
string businessType = "",
long? businessId = null)
[HttpPost]
public async Task<SendSystemMessageOutput> 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
/// 标记消息为已读
/// 业务场景:用户查看消息后更新状态,用于消息中心的已读/未读状态管理
/// </summary>
public async Task<bool> MarkMessageAsReadAsync(long messageId, long recipientPersonnelId)
[HttpPost]
public async Task<SystemMessageStatusOutput> 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
/// 批量标记消息为已读
/// 业务场景:用户批量处理消息,提高操作效率
/// </summary>
public async Task<int> BatchMarkMessagesAsReadAsync(List<long> messageIds, long recipientPersonnelId)
[HttpPost]
public async Task<SystemMessageStatusOutput> BatchMarkMessagesAsReadAsync(List<long> 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
/// 删除消息
/// 业务场景:用户清理不需要的消息,实现软删除以保留审计记录
/// </summary>
public async Task<bool> DeleteMessageAsync(long messageId, long recipientPersonnelId)
[HttpPost]
public async Task<SystemMessageStatusOutput> 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
/// <summary>
/// 将SystemMessageTypeEnum转换为通知历史实体的MessageType字段
/// </summary>
private string GetNotificationMessageType(SystemMessageTypeEnum messageType)
{
return messageType switch
{
SystemMessageTypeEnum.Info => "信息",
SystemMessageTypeEnum.Success => "成功",
SystemMessageTypeEnum.Warning => "警告",
SystemMessageTypeEnum.Error => "错误",
SystemMessageTypeEnum.Urgent => "紧急",
_ => "信息"
};
}
// 注:原有的消息类型转换方法已移除,因为实体结构调整后不再需要
#endregion
}

View File

@ -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;
}
/// <summary>
@ -314,4 +318,127 @@ public class PersonnelQualificationService : BaseService, IPersonnelQualificatio
return new List<PersonnelQualificationEntity>();
}
}
/// <summary>
/// 获取人员资质统计信息(横坐标为资质,纵坐标为人员数量)
/// </summary>
/// <returns>人员资质统计结果</returns>
public async Task<PersonnelQualificationStatisticsResult> 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<PersonnelQualificationStatistics>(),
GeneratedTime = DateTime.Now
};
}
}
/// <summary>
/// 获取预警期内即将到期的人员资质清单
/// </summary>
/// <param name="today">基准日期(通常为 DateTime.Today</param>
/// <param name="personnelIds">限定人员范围(可选)</param>
/// <param name="includeExpired">是否包含已过期记录</param>
/// <returns></returns>
public async Task<List<ExpiringQualificationItemOutput>> GetExpiringAsync(DateTime today, IList<long> 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<long, string>()
: (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<ExpiringQualificationItemOutput>(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();
}
}

View File

@ -440,7 +440,7 @@ public class WorkOrderService : BaseService, IWorkOrderService, IDynamicApi
/// </summary>
/// <param name="workOrderEntity">待转换的工作任务实体</param>
/// <returns>转换后的工作任务实体,包含计算好的计划开始和结束时间</returns>
private async Task<WorkOrderEntity> ConvertWorkOrderShift(WorkOrderEntity workOrderEntity)
public async Task<WorkOrderEntity> ConvertWorkOrderShift(WorkOrderEntity workOrderEntity)
{
// 【业务逻辑】根据任务的班次ID获取班次定义信息
// 班次定义包含: StartTime、EndTime、班次名称等基础配置
@ -475,6 +475,20 @@ public class WorkOrderService : BaseService, IWorkOrderService, IDynamicApi
#endregion
public async Task<Dictionary<long, int>> 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<long, int>(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
/// <summary>

View File

@ -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;
/// <summary>
/// 工作台服务实现
/// 提供工作台相关的数据查询和业务逻辑处理
/// </summary>
[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;
}
/// <summary>
/// 获取用户本周日历数据
/// 包含任务排班信息和班次不可用性信息,用于日历视图展示
/// </summary>
/// <param name="input">查询参数</param>
/// <returns>本周日历数据</returns>
[HttpPost]
public async Task<WeeklyCalendarOutput> 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<ShiftUnavailabilityEntity>());
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<CalendarDayItem>()
};
// 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<CalendarShiftItem>()
};
// 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<CalendarTaskItem>(),
UnavailableItems = new List<CalendarUnavailableItem>()
};
// 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
/// <summary>
/// 获取指定日期所在周的周一日期
/// </summary>
/// <param name="date">指定日期</param>
/// <returns>周一日期</returns>
private static DateTime GetWeekStartDate(DateTime date)
{
var dayOfWeek = (int)date.DayOfWeek;
if (dayOfWeek == 0) dayOfWeek = 7; // 将周日从0调整为7
return date.AddDays(-(dayOfWeek - 1)).Date;
}
/// <summary>
/// 获取星期几的中文名称
/// </summary>
/// <param name="dayOfWeek">星期几1=周一7=周日)</param>
/// <returns>中文名称</returns>
private static string GetDayOfWeekName(int dayOfWeek)
{
return dayOfWeek switch
{
1 => "周一",
2 => "周二",
3 => "周三",
4 => "周四",
5 => "周五",
6 => "周六",
7 => "周日",
_ => ""
};
}
/// <summary>
/// 获取指定人员本周的工作任务
/// </summary>
/// <param name="personnelId">人员ID</param>
/// <param name="weekStartDate">周开始日期</param>
/// <param name="weekEndDate">周结束日期</param>
/// <param name="includeCompleted">是否包含已完成任务</param>
/// <returns>任务列表</returns>
private async Task<List<WorkOrderEntity>> 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;
}
/// <summary>
/// 获取指定人员本周的班次不可用记录
/// </summary>
/// <param name="personnelId">人员ID</param>
/// <param name="weekStartDate">周开始日期</param>
/// <param name="weekEndDate">周结束日期</param>
/// <returns>不可用记录列表</returns>
private async Task<List<ShiftUnavailabilityEntity>> 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;
}
/// <summary>
/// 获取所有活跃的班次
/// </summary>
/// <returns>班次列表</returns>
private async Task<List<ShiftEntity>> GetAllActiveShiftsAsync()
{
var shifts = await _shiftRepository.Where(s => s.IsEnabled)
.OrderBy(s => s.StartTime)
.ToListAsync();
return shifts;
}
#endregion
}

View File

@ -0,0 +1,13 @@
{
"version": 1,
"isRoot": true,
"tools": {
"dotnet-ef": {
"version": "9.0.9",
"commands": [
"dotnet-ef"
],
"rollForward": false
}
}
}

View File

@ -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": "排班服务",

View File

@ -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]"

View File

@ -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<RabbitMQOptions>();
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();
//<PackageReference Include="DotNetCore.CAP.MySql" Version="8.3.2" />
//<PackageReference Include="DotNetCore.CAP.RabbitMQ" Version="8.3.2" />
//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) =>

View File

@ -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",

View File

@ -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 @@
//}
]
}
}
}

View File

@ -14,6 +14,7 @@
</PackageReference>
<PackageReference Include="Microsoft.AspNetCore.Mvc.Testing" Version="9.0.5"/>
<PackageReference Include="Microsoft.AspNetCore.TestHost" Version="9.0.5"/>
<PackageReference Include="Moq" Version="4.20.72"/>
</ItemGroup>
<ItemGroup>

View File

@ -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<AlternativeCandidate> 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中的先进架构模式为后续的业务扩展和系统优化奠定了坚实基础。