docs: 添加项目 README 文档,详细描述项目概述。
This commit is contained in:
@@ -1,453 +1,519 @@
|
||||
using AutoNotificatPhone.Controllers;
|
||||
using AutoNotificatPhone.Controllers;
|
||||
using Common;
|
||||
using System.Threading;
|
||||
using NLog;
|
||||
using System.Diagnostics;
|
||||
using Npgsql;
|
||||
using Microsoft.Extensions.Configuration;
|
||||
using NLog;
|
||||
using Npgsql;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace AutoNotificatPhone.Models
|
||||
{
|
||||
/// <summary>
|
||||
/// 定时后台服务:
|
||||
/// 1) 每分钟固定时刻执行巡检
|
||||
/// 2) 执行整点/定时通知
|
||||
/// 3) 执行 Redis 指标告警
|
||||
/// 4) 执行 PostgreSQL 心跳检查告警
|
||||
/// </summary>
|
||||
public class TimerClass : BackgroundService
|
||||
{
|
||||
// 日志记录器
|
||||
public static Logger logger = LogManager.GetCurrentClassLogger();
|
||||
// 通知接收手机号码
|
||||
// NLog 记录器
|
||||
private static readonly Logger Logger = LogManager.GetCurrentClassLogger();
|
||||
|
||||
// 告警通知接收手机号
|
||||
private static readonly string Mobile1 = "13509214696";
|
||||
private static readonly string Mobile2 = "16620970520";
|
||||
// 记录已执行任务的时间点
|
||||
private Dictionary<DateTime, bool> _executedTasks = [];
|
||||
// 消息控制器实例
|
||||
private readonly CallAndMsgController _callAndMsgController = new();
|
||||
private readonly IConfiguration _configuration;
|
||||
// Kafka数据库连接失败累计次数
|
||||
private int _kafkaDbConnectionAlertCount = 0;
|
||||
|
||||
// 每日定时任务触发小时(北京时间)
|
||||
private static readonly HashSet<int> DailyTaskHours = [10, 15, 22];
|
||||
|
||||
// 每分钟在第 30 秒触发一次巡检
|
||||
private const int RunSecond = 30;
|
||||
// 主循环异常后的重试等待时间(秒)
|
||||
private const int RetryDelaySeconds = 10;
|
||||
|
||||
// 默认短信/电话任务过期时间(秒)
|
||||
private const int SmsDeadlineSeconds = 1800;
|
||||
private const int CallDeadlineSeconds = 900;
|
||||
// 扩展短信/电话任务过期时间(秒)
|
||||
private const int ExtendedSmsDeadlineSeconds = 3600;
|
||||
private const int ExtendedCallDeadlineSeconds = 1800;
|
||||
|
||||
// Kafka 心跳超时阈值(分钟)
|
||||
private const int KafkaStaleMinutes = 5;
|
||||
// 数据库连接失败累计到 N 次才触发一次告警,避免告警风暴
|
||||
private const int KafkaDbAlertTriggerCount = 8;
|
||||
|
||||
// 接收包“低值”判定阈值
|
||||
private const int RecvPackageLowThreshold = 70000;
|
||||
|
||||
// 用于防止同一整点任务重复执行
|
||||
private readonly Dictionary<DateTime, bool> _executedTasks = new();
|
||||
// 复用 API 控制器发送短信/电话任务
|
||||
private readonly CallAndMsgController _callAndMsgController = new();
|
||||
// 从 appsettings 读取 Postgres 配置
|
||||
private readonly IConfiguration _configuration;
|
||||
|
||||
// Kafka 数据库连接失败计数器
|
||||
private int _kafkaDbConnectionAlertCount;
|
||||
|
||||
/// <summary>
|
||||
/// 构造函数,注入配置对象。
|
||||
/// </summary>
|
||||
/// <param name="configuration">应用配置(用于读取 Postgres 节点)</param>
|
||||
public TimerClass(IConfiguration configuration)
|
||||
{
|
||||
_configuration = configuration;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 后台服务主执行方法
|
||||
/// 后台服务主循环。
|
||||
/// </summary>
|
||||
/// <param name="cancellationToken">服务取消令牌</param>
|
||||
protected override async Task ExecuteAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
await Task.Factory.StartNew(async () =>
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
try
|
||||
{
|
||||
try
|
||||
{
|
||||
// 计算下次执行时间(每分钟的30秒)
|
||||
var now = DateTime.UtcNow;
|
||||
var nextRunTime = CalculateNextRunTime(now);
|
||||
var delayTime = nextRunTime - now;
|
||||
// 等待到下一次固定执行时刻(每分钟第 RunSecond 秒)
|
||||
await DelayUntilNextRunAsync(cancellationToken);
|
||||
|
||||
// 等待到下一个执行点
|
||||
await Task.Delay(delayTime, cancellationToken);
|
||||
// 检查电话机进程在线状态并写日志
|
||||
LogPhoneStatus(CheckPhoneIsOnline());
|
||||
|
||||
// 检查电话机状态并记录日志
|
||||
LogPhoneStatus(CheckPhoneIsOnline());
|
||||
// 执行整点/每日通知任务
|
||||
RunHourlyNotificationTask();
|
||||
|
||||
// 执行定时任务
|
||||
await RegularlySendActiverecords();
|
||||
|
||||
// 执行系统检查任务
|
||||
await CheckCpuThresholdAsync();
|
||||
await CheckRcuOnlineAsync();
|
||||
await CheckTotalSendPackageAsync();
|
||||
await CheckTotalRecvPackageAsync();
|
||||
await CheckKafkaHeartbeatAsync();
|
||||
}
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// 任务被取消时的处理
|
||||
logger.Error("任务被取消");
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 异常处理
|
||||
logger.Error($"主循环发生错误: {ex.Message}");
|
||||
await Task.Delay(TimeSpan.FromSeconds(10), cancellationToken);
|
||||
}
|
||||
// 执行各项系统检查任务
|
||||
CheckCpuThreshold();
|
||||
CheckRcuOnline();
|
||||
CheckTotalSendPackage();
|
||||
CheckTotalRecvPackage();
|
||||
await CheckKafkaHeartbeatAsync();
|
||||
}
|
||||
}, TaskCreationOptions.LongRunning);
|
||||
catch (TaskCanceledException)
|
||||
{
|
||||
// 服务停止时会进入这里
|
||||
Logger.Error("任务被取消");
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 主循环兜底异常,稍后重试
|
||||
Logger.Error($"主循环发生错误: {ex.Message}");
|
||||
await Task.Delay(TimeSpan.FromSeconds(RetryDelaySeconds), cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 计算下次执行时间(每分钟的30秒)
|
||||
/// 等待到下一次固定执行时间。
|
||||
/// </summary>
|
||||
private DateTime CalculateNextRunTime(DateTime now)
|
||||
/// <param name="cancellationToken">取消令牌</param>
|
||||
private async Task DelayUntilNextRunAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
var nextRunTime = new DateTime(now.Year, now.Month, now.Day, now.Hour, now.Minute, 30);
|
||||
return now.Second >= 30 ? nextRunTime.AddMinutes(1) : nextRunTime;
|
||||
var now = DateTime.UtcNow;
|
||||
var nextRunTime = CalculateNextRunTime(now);
|
||||
var delayTime = nextRunTime - now;
|
||||
// 到点前阻塞等待
|
||||
await Task.Delay(delayTime, cancellationToken);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 记录电话机状态日志
|
||||
/// 计算下一次执行时间点(每分钟第 RunSecond 秒)。
|
||||
/// </summary>
|
||||
private void LogPhoneStatus(bool isOnline)
|
||||
/// <param name="now">当前 UTC 时间</param>
|
||||
/// <returns>下一次执行时间</returns>
|
||||
private static DateTime CalculateNextRunTime(DateTime now)
|
||||
{
|
||||
logger.Error(isOnline ? "电话机在线,开始判断!+++++str+++++" : "电话机不在线,下面内容可能不会执行!+++++err+++++");
|
||||
var nextRunTime = new DateTime(now.Year, now.Month, now.Day, now.Hour, now.Minute, RunSecond);
|
||||
// 如果当前秒已过触发点,则顺延到下一分钟
|
||||
return now.Second >= RunSecond ? nextRunTime.AddMinutes(1) : nextRunTime;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查电话机进程是否在线
|
||||
/// 输出电话机在线状态日志。
|
||||
/// </summary>
|
||||
private bool CheckPhoneIsOnline()
|
||||
/// <param name="isOnline">是否在线</param>
|
||||
private static void LogPhoneStatus(bool isOnline)
|
||||
{
|
||||
Logger.Error(isOnline
|
||||
? "电话机在线,开始判断!+++++str+++++"
|
||||
: "电话机不在线,下面内容可能不会执行!+++++err+++++");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过本机进程名判断电话机程序是否运行。
|
||||
/// </summary>
|
||||
/// <returns>在线返回 true,否则 false</returns>
|
||||
private static bool CheckPhoneIsOnline()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 通过进程名检查电话机是否运行
|
||||
// 约定进程名为 Telephone
|
||||
return Process.GetProcessesByName("Telephone").Length > 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error($"电话机进程检查失败: {ex.Message}");
|
||||
Logger.Error($"电话机进程检查失败: {ex.Message}");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 定时发送活动记录(整点任务)
|
||||
/// 整点通知任务调度(北京时间):
|
||||
/// - 非整点直接返回
|
||||
/// - 同一整点只执行一次
|
||||
/// - 10/15/22 点执行每日任务,其余整点执行整点短信
|
||||
/// </summary>
|
||||
private async Task RegularlySendActiverecords()
|
||||
private void RunHourlyNotificationTask()
|
||||
{
|
||||
// 转换为北京时间
|
||||
// 当前北京时间
|
||||
var beijingTime = DateTime.UtcNow.AddHours(8);
|
||||
//logger.Error($"每日任务检查 - 当前北京时间: {beijingTime:yyyy-MM-dd HH:mm:ss}");
|
||||
|
||||
// 检查是否整点
|
||||
// 仅整点触发
|
||||
if (beijingTime.Minute != 0)
|
||||
{
|
||||
//logger.Error($"不满足整点任务触发条件 - 当前时间: {beijingTime:HH:mm}");
|
||||
return;
|
||||
}
|
||||
|
||||
// 创建整点时间键
|
||||
// 当前整点键,用于去重
|
||||
var hourlyKey = new DateTime(beijingTime.Year, beijingTime.Month, beijingTime.Day, beijingTime.Hour, 0, 0);
|
||||
|
||||
// 检查任务是否已执行
|
||||
if (_executedTasks.ContainsKey(hourlyKey))
|
||||
{
|
||||
//logger.Error($"跳过已执行的整点任务 - 时间点: {hourlyKey:yyyy-MM-dd HH:mm}");
|
||||
// 避免重复执行
|
||||
return;
|
||||
}
|
||||
|
||||
logger.Error($"准备执行整点短信任务 - 时间点: {hourlyKey:yyyy-MM-dd HH:mm}");
|
||||
Logger.Error($"准备执行整点短信任务 - 时间点: {hourlyKey:yyyy-MM-dd HH:mm}");
|
||||
|
||||
// 判断执行每日任务还是整点短信
|
||||
if (beijingTime.Hour is 10 or 15 or 22)
|
||||
// 每日固定时点执行“每日任务”,否则执行“整点短信”
|
||||
if (DailyTaskHours.Contains(beijingTime.Hour))
|
||||
{
|
||||
await ExecuteDailyTask(beijingTime);
|
||||
ExecuteDailyTask(beijingTime);
|
||||
}
|
||||
else
|
||||
{
|
||||
await SendHourlySms(beijingTime);
|
||||
SendHourlySms(beijingTime);
|
||||
}
|
||||
|
||||
// 标记任务已执行
|
||||
// 标记当前整点已执行
|
||||
_executedTasks[hourlyKey] = true;
|
||||
|
||||
// 清理过期任务记录
|
||||
// 清理历史日期记录
|
||||
CleanupOldTasks(beijingTime);
|
||||
|
||||
//logger.Error($"整点任务执行完成 - 时间点: {hourlyKey:yyyy-MM-dd HH:mm}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 清理过期任务记录
|
||||
/// 清理前一天及更早的整点执行记录。
|
||||
/// </summary>
|
||||
/// <param name="currentTime">当前时间(北京时间)</param>
|
||||
private void CleanupOldTasks(DateTime currentTime)
|
||||
{
|
||||
// 找出所有过期的任务键
|
||||
var keysToRemove = _executedTasks.Keys.Where(k => k.Date < currentTime.Date).ToList();
|
||||
var keysToRemove = _executedTasks.Keys.Where(key => key.Date < currentTime.Date).ToList();
|
||||
foreach (var key in keysToRemove)
|
||||
{
|
||||
_executedTasks.Remove(key);
|
||||
//logger.Error($"清理过期任务记录: {key:yyyy-MM-dd HH:mm}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 发送整点短信
|
||||
/// 发送整点短信。
|
||||
/// </summary>
|
||||
private async Task SendHourlySms(DateTime beijingTime)
|
||||
/// <param name="beijingTime">当前北京时间</param>
|
||||
private void SendHourlySms(DateTime beijingTime)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 准备短信内容
|
||||
long currentTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
string dateTimeStr = $"{beijingTime.Month}月{beijingTime.Day}日{beijingTime.Hour}点";
|
||||
string smsContent = $"[BLV运维提示] 整点系统状态报告。当前时间:{dateTimeStr}";
|
||||
// 生成展示时间文本
|
||||
var currentTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
var dateTimeStr = $"{beijingTime.Month}月{beijingTime.Day}日{beijingTime.Hour}点";
|
||||
var smsContent = $"[BLV运维提示] 整点系统状态报告。当前时间:{dateTimeStr}";
|
||||
|
||||
// 创建短信请求
|
||||
// 仅发送短信到 Mobile1
|
||||
var request = CreateSmsRequest(
|
||||
type: "2",
|
||||
deadline: currentTimestamp + 1800,
|
||||
deadline: currentTimestamp + SmsDeadlineSeconds,
|
||||
phone: Mobile1,
|
||||
caller: "整点报告",
|
||||
content: smsContent
|
||||
);
|
||||
content: smsContent);
|
||||
|
||||
// 发送短信并记录结果
|
||||
var result = _callAndMsgController.SendToPhone(request);
|
||||
/*logger.Error(result.isok
|
||||
? $"整点短信已发送:{dateTimeStr}"
|
||||
: $"发送整点短信失败:{result.message}");*/
|
||||
// 投递短信任务
|
||||
_callAndMsgController.SendToPhone(request);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error($"发送整点短信时出错:{ex.Message}");
|
||||
Logger.Error($"发送整点短信时出错:{ex.Message}");
|
||||
}
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行每日定时任务(10点、15点、22点)
|
||||
/// 执行每日定时通知(短信 + 电话)。
|
||||
/// </summary>
|
||||
private async Task ExecuteDailyTask(DateTime beijingTime)
|
||||
/// <param name="beijingTime">当前北京时间</param>
|
||||
private void ExecuteDailyTask(DateTime beijingTime)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 准备消息内容
|
||||
long currentTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
string dateTimeStr = $"{beijingTime.Month}月{beijingTime.Day}日{beijingTime.Hour}点";
|
||||
string smsContent = $"[BLV运维提示] 每日定时通知。当前时间为:{dateTimeStr}";
|
||||
string callContent = $"BLV运维提示 每日定时通知 当前时间为 {dateTimeStr}";
|
||||
var currentTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
var dateTimeStr = $"{beijingTime.Month}月{beijingTime.Day}日{beijingTime.Hour}点";
|
||||
var smsContent = $"[BLV运维提示] 每日定时通知。当前时间为:{dateTimeStr}";
|
||||
var callContent = $"BLV运维提示 每日定时通知 当前时间为 {dateTimeStr}";
|
||||
|
||||
// 创建短信和电话请求
|
||||
var smsRequest1 = CreateSmsRequest("2", currentTimestamp + 1800, Mobile1, "每日定时通知", smsContent);
|
||||
var smsRequest2 = CreateSmsRequest("2", currentTimestamp + 1800, Mobile2, "每日定时通知", smsContent);
|
||||
var callRequest = CreateSmsRequest("1", currentTimestamp + 900, Mobile1, "每日定时通知", callContent);
|
||||
// 两路短信 + 一路电话
|
||||
var smsRequest1 = CreateSmsRequest("2", currentTimestamp + SmsDeadlineSeconds, Mobile1, "每日定时通知", smsContent);
|
||||
var smsRequest2 = CreateSmsRequest("2", currentTimestamp + SmsDeadlineSeconds, Mobile2, "每日定时通知", smsContent);
|
||||
var callRequest = CreateSmsRequest("1", currentTimestamp + CallDeadlineSeconds, Mobile1, "每日定时通知", callContent);
|
||||
|
||||
// 发送通知
|
||||
// 执行发送并检查结果
|
||||
var smsResult1 = _callAndMsgController.SendToPhone(smsRequest1);
|
||||
var smsResult2 = _callAndMsgController.SendToPhone(smsRequest2);
|
||||
var callResult = _callAndMsgController.SendToPhone(callRequest);
|
||||
|
||||
// 记录发送结果
|
||||
if (smsResult1.isok && smsResult2.isok && callResult.isok)
|
||||
if (!smsResult1.isok || !smsResult2.isok || !callResult.isok)
|
||||
{
|
||||
//logger.Error($"每日定时通知已发送:{dateTimeStr}");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error($"发送每日定时通知失败:短信1={smsResult1.message} 短信2={smsResult2.message} 电话={callResult.message}");
|
||||
Logger.Error($"发送每日定时通知失败:短信1={smsResult1.message} 短信2={smsResult2.message} 电话={callResult.message}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error($"执行每日任务时出错:{ex.Message}");
|
||||
Logger.Error($"执行每日任务时出错:{ex.Message}");
|
||||
}
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查CPU使用率阈值
|
||||
/// CPU 阈值检查:
|
||||
/// 1) 先判断监控程序是否失联
|
||||
/// 2) 再判断 CPU 指标是否超过阈值
|
||||
/// </summary>
|
||||
private async Task CheckCpuThresholdAsync()
|
||||
private void CheckCpuThreshold()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 检查监控程序是否在线
|
||||
string detectTimeString = CSRedisCacheHelper.redis1.Get<string>("UDPPackage_DetectTime");
|
||||
// 监控程序最后上报时间(来自 Redis)
|
||||
var detectTimeString = CSRedisCacheHelper.redis1.Get<string>("UDPPackage_DetectTime");
|
||||
if (!string.IsNullOrEmpty(detectTimeString) &&
|
||||
DateTime.TryParse(detectTimeString, out DateTime detectTime) &&
|
||||
DateTime.TryParse(detectTimeString, out var detectTime) &&
|
||||
(DateTime.UtcNow - detectTime).TotalMinutes > 10)
|
||||
{
|
||||
// 监控程序无法访问警报
|
||||
// 超过 10 分钟未更新,触发监控失联告警
|
||||
ExecuteMonitorUnavailableAlert(detectTime);
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取CPU使用率数据
|
||||
string cpuMax = CSRedisCacheHelper.redis1.Get<string>("UDPPackage_CPUMax");
|
||||
string cpuAvg = CSRedisCacheHelper.redis1.Get<string>("UDPPackage_CPUAvg");
|
||||
List<int> cpuMaxValues = ParseCpuValues(cpuMax);
|
||||
List<int> cpuAvgValues = ParseCpuValues(cpuAvg);
|
||||
// 拉取 CPU 指标
|
||||
var cpuMax = CSRedisCacheHelper.redis1.Get<string>("UDPPackage_CPUMax");
|
||||
var cpuAvg = CSRedisCacheHelper.redis1.Get<string>("UDPPackage_CPUAvg");
|
||||
|
||||
// 记录CPU使用率
|
||||
//logger.Error($"RCU服务器的CPU使用率-AVG:{cpuAvg},MAX:{cpuMax}");
|
||||
var cpuMaxValues = ParseCsvToIntList(cpuMax);
|
||||
var cpuAvgValues = ParseCsvToIntList(cpuAvg);
|
||||
|
||||
// 检查是否超过阈值
|
||||
if (CheckThreshold(cpuAvgValues, 80, 6))//(CheckThreshold(cpuMaxValues, 90, 6) || CheckThreshold(cpuAvgValues, 85, 6))
|
||||
// 规则:平均 CPU >= 80 的点达到 6 个触发告警
|
||||
if (CheckThreshold(cpuAvgValues, threshold: 80, requiredCount: 6))
|
||||
{
|
||||
// 触发CPU警报
|
||||
ExecuteCpuAlert(cpuMaxValues, ParseCpuValues(CSRedisCacheHelper.redis1.Get<string>("UDPPackage_CPUMin")), cpuAvgValues);
|
||||
var cpuMinValues = ParseCsvToIntList(CSRedisCacheHelper.redis1.Get<string>("UDPPackage_CPUMin"));
|
||||
ExecuteCpuAlert(cpuMaxValues, cpuMinValues, cpuAvgValues);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error($"CPU阈值检查错误: {ex.Message}");
|
||||
Logger.Error($"CPU阈值检查错误: {ex.Message}");
|
||||
}
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查RCU在线数量
|
||||
/// 检查 RCU 在线数量。
|
||||
/// </summary>
|
||||
private async Task CheckRcuOnlineAsync()
|
||||
private void CheckRcuOnline()
|
||||
{
|
||||
await CheckRedisValueAsync("RCUOnLine", 8, 0.8, ExecuteRcuOnlineAlert, "RCU主机的在线数量");
|
||||
CheckRedisValue(
|
||||
redisKey: "RCUOnLine",
|
||||
baselineCount: 8,
|
||||
thresholdRatio: 0.8,
|
||||
alertAction: ExecuteRcuOnlineAlert,
|
||||
logPrefix: "RCU主机的在线数量");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查总发送包数量
|
||||
/// 检查 RCU 总发送包数量。
|
||||
/// </summary>
|
||||
private async Task CheckTotalSendPackageAsync()
|
||||
private void CheckTotalSendPackage()
|
||||
{
|
||||
await CheckRedisValueAsync("UDPPackage_TotalSendPackage", 8, 0.6, ExecuteTotalSendPackageAlert, "RCU主机的通讯数量");
|
||||
CheckRedisValue(
|
||||
redisKey: "UDPPackage_TotalSendPackage",
|
||||
baselineCount: 8,
|
||||
thresholdRatio: 0.6,
|
||||
alertAction: ExecuteTotalSendPackageAlert,
|
||||
logPrefix: "RCU主机的通讯数量");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查总接收包数量
|
||||
/// 检查 RCU 总接收包数量。
|
||||
/// 除了通用阈值逻辑外,还增加“最后 3 个值都低于固定阈值”的快速告警。
|
||||
/// </summary>
|
||||
private async Task CheckTotalRecvPackageAsync()
|
||||
private void CheckTotalRecvPackage()
|
||||
{
|
||||
try
|
||||
{
|
||||
// 从Redis获取值
|
||||
string valueString = CSRedisCacheHelper.redis1.Get<string>("UDPPackage_TotalRecvPackage");
|
||||
if (string.IsNullOrEmpty(valueString)) return;
|
||||
|
||||
// 解析值
|
||||
List<int> values = ParseCpuValues(valueString);
|
||||
if (values == null || values.Count < 10) return;
|
||||
|
||||
// 检查最后3个值是否都小于70000
|
||||
if (values.Count >= 3 && values[^3] < 70000 && values[^2] < 70000 && values[^1] < 70000)
|
||||
// 获取接收包时序数据
|
||||
var valueString = CSRedisCacheHelper.redis1.Get<string>("UDPPackage_TotalRecvPackage");
|
||||
if (string.IsNullOrEmpty(valueString))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var values = ParseCsvToIntList(valueString);
|
||||
if (values.Count < 10)
|
||||
{
|
||||
// 数据点不足,无法按规则判定
|
||||
return;
|
||||
}
|
||||
|
||||
// 特殊规则:最后 3 个点都很低,立即告警
|
||||
if (values.Count >= 3 && values[^3] < RecvPackageLowThreshold && values[^2] < RecvPackageLowThreshold && values[^1] < RecvPackageLowThreshold)
|
||||
{
|
||||
// 触发警报
|
||||
ExecuteTotalRecvPackageAlert([values[^3], values[^2], values[^1]]);
|
||||
return;
|
||||
}
|
||||
else
|
||||
{
|
||||
// 原有的阈值检查逻辑
|
||||
await CheckRedisValueAsync("UDPPackage_TotalRecvPackage", 8, 0.75, ExecuteTotalRecvPackageAlert, "RCU主机的通讯数量");
|
||||
}
|
||||
|
||||
// 回退到通用阈值规则
|
||||
CheckRedisValue(
|
||||
redisKey: "UDPPackage_TotalRecvPackage",
|
||||
baselineCount: 8,
|
||||
thresholdRatio: 0.75,
|
||||
alertAction: ExecuteTotalRecvPackageAlert,
|
||||
logPrefix: "RCU主机的通讯数量");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error($"总接收包数量检查错误: {ex.Message}");
|
||||
Logger.Error($"总接收包数量检查错误: {ex.Message}");
|
||||
}
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通用Redis值检查方法
|
||||
/// 通用 Redis 时序指标检查:
|
||||
/// - 以前 baselineCount 个点的平均值作为基线
|
||||
/// - 计算阈值(平均值 * thresholdRatio)
|
||||
/// - 若后续两个点都低于阈值则触发告警
|
||||
/// </summary>
|
||||
private async Task CheckRedisValueAsync(string redisKey, int baselineCount, double thresholdRatio, Action<List<int>> alertAction, string logPrefix)
|
||||
/// <param name="redisKey">Redis 键</param>
|
||||
/// <param name="baselineCount">基线样本数量</param>
|
||||
/// <param name="thresholdRatio">阈值比例</param>
|
||||
/// <param name="alertAction">告警动作</param>
|
||||
/// <param name="logPrefix">日志前缀</param>
|
||||
private void CheckRedisValue(string redisKey, int baselineCount, double thresholdRatio, Action<List<int>> alertAction, string logPrefix)
|
||||
{
|
||||
try
|
||||
{
|
||||
// 从Redis获取值
|
||||
string valueString = CSRedisCacheHelper.redis1.Get<string>(redisKey);
|
||||
if (string.IsNullOrEmpty(valueString)) return;
|
||||
// 从 Redis 读取 CSV 字符串
|
||||
var valueString = CSRedisCacheHelper.redis1.Get<string>(redisKey);
|
||||
if (string.IsNullOrEmpty(valueString))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 解析值
|
||||
List<int> values = ParseCpuValues(valueString);
|
||||
if (values == null || values.Count < 10) return;
|
||||
var values = ParseCsvToIntList(valueString);
|
||||
if (values.Count < 10)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 计算平均值和阈值
|
||||
double average = values.Take(baselineCount).Average();
|
||||
double threshold = average * thresholdRatio;
|
||||
//logger.Error($"{logPrefix}-AVG:{average},ALL:{valueString}");
|
||||
// 计算阈值
|
||||
var average = values.Take(baselineCount).Average();
|
||||
var threshold = average * thresholdRatio;
|
||||
|
||||
// 检查最后两个值是否低于阈值
|
||||
// 后续两个点均低于阈值才触发
|
||||
if (values[baselineCount] < threshold && values[baselineCount + 1] < threshold)
|
||||
{
|
||||
// 触发警报
|
||||
alertAction(values);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error($"{logPrefix}检查错误: {ex.Message}");
|
||||
Logger.Error($"{logPrefix}检查错误: {ex.Message}");
|
||||
}
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行RCU在线数量警报
|
||||
/// RCU 在线数量告警。
|
||||
/// </summary>
|
||||
private void ExecuteRcuOnlineAlert(List<int> values)
|
||||
{
|
||||
SendAlert(
|
||||
smsContent: $"[BLV运维提示] RCU主机在线数量低于正常值,请立即检查。数据:{string.Join(",", values)}",
|
||||
callContent: "BLV运维提示 RCU主机在线数量低于正常值 请立即检查",
|
||||
alertType: "RCU-在线数量警报"
|
||||
);
|
||||
alertType: "RCU-在线数量警报");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行总发送包数量警报
|
||||
/// RCU 发送数量告警。
|
||||
/// </summary>
|
||||
private void ExecuteTotalSendPackageAlert(List<int> values)
|
||||
{
|
||||
SendAlert(
|
||||
smsContent: $"[BLV运维提示] RCU发送数量低于预期值,请立即检查。数据:{string.Join(",", values)}",
|
||||
callContent: "BLV运维提示 RCU发送数量低于预期值 请立即检查",
|
||||
alertType: "RCU-通讯数量警报"
|
||||
);
|
||||
alertType: "RCU-通讯数量警报");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行总接收包数量警报
|
||||
/// RCU 接收数量告警。
|
||||
/// </summary>
|
||||
private void ExecuteTotalRecvPackageAlert(List<int> values)
|
||||
{
|
||||
SendAlert(
|
||||
smsContent: $"[BLV运维提示] RCU接收数量低于预期值,请立即检查。数据:{string.Join(",", values)}",
|
||||
callContent: "BLV运维提示 RCU接收数量低于预期值 请立即检查",
|
||||
alertType: "RCU-通讯数量警报"
|
||||
);
|
||||
alertType: "RCU-通讯数量警报");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行CPU使用率警报
|
||||
/// CPU 告警。
|
||||
/// </summary>
|
||||
private void ExecuteCpuAlert(List<int> cpuMax, List<int> cpuMin, List<int> cpuAvg)
|
||||
{
|
||||
string dataString = $"AVG:{string.Join(",", cpuAvg)},MAX:{string.Join(",", cpuMax)},MIN:{string.Join(",", cpuMin)}";
|
||||
// 拼接 CPU 指标明细
|
||||
var dataString = $"AVG:{string.Join(",", cpuAvg)},MAX:{string.Join(",", cpuMax)},MIN:{string.Join(",", cpuMin)}";
|
||||
SendAlert(
|
||||
smsContent: $"[BLV运维提示] RCU服务器的CPU使用率告警。{dataString}",
|
||||
callContent: "BLV运维提示 RCU服务器的CPU使用率告警 请立即检查",
|
||||
alertType: "RCU-CPU警报"
|
||||
);
|
||||
alertType: "RCU-CPU警报");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行监控程序无法访问警报
|
||||
/// 监控程序失联告警。
|
||||
/// </summary>
|
||||
/// <param name="detectTime">最后检测时间(当前版本仅用于语义传参)</param>
|
||||
private void ExecuteMonitorUnavailableAlert(DateTime detectTime)
|
||||
{
|
||||
SendAlert(
|
||||
smsContent: "[BLV运维提示] RCU服务器的监控程序无法访问,请立即检查。",
|
||||
callContent: "BLV运维提示 RCU服务器的监控程序无法访问 请立即检查",
|
||||
alertType: "RCU-监控程序警报",
|
||||
extendedDeadline: true
|
||||
);
|
||||
extendedDeadline: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查Kafka入库心跳(PostgreSQL)
|
||||
/// 检查 PostgreSQL 心跳表,判断 Kafka 入库是否活跃。
|
||||
/// </summary>
|
||||
private async Task CheckKafkaHeartbeatAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
string? connectionString = BuildPostgresConnectionString();
|
||||
// 从配置构建连接串
|
||||
var connectionString = BuildPostgresConnectionString();
|
||||
if (string.IsNullOrWhiteSpace(connectionString))
|
||||
{
|
||||
logger.Error("Postgres配置缺失,无法检查Kafka入库心跳");
|
||||
Logger.Error("Postgres配置缺失,无法检查Kafka入库心跳");
|
||||
// 配置缺失等价于连接失败告警路径
|
||||
ExecuteKafkaDbConnectionAlert();
|
||||
return;
|
||||
}
|
||||
|
||||
// 建立数据库连接
|
||||
await using var connection = new NpgsqlConnection(connectionString);
|
||||
await connection.OpenAsync();
|
||||
|
||||
// 查询最近数据中的最新 write_ts_ms
|
||||
const string sql = @"SELECT write_ts_ms
|
||||
FROM (
|
||||
SELECT write_ts_ms
|
||||
@@ -457,52 +523,59 @@ namespace AutoNotificatPhone.Models
|
||||
) AS recent_events
|
||||
ORDER BY write_ts_ms DESC
|
||||
LIMIT 1;";
|
||||
await using var command = new NpgsqlCommand(sql, connection);
|
||||
object? result = await command.ExecuteScalarAsync();
|
||||
|
||||
await using var command = new NpgsqlCommand(sql, connection);
|
||||
var result = await command.ExecuteScalarAsync();
|
||||
|
||||
// 空结果按数据库异常路径处理
|
||||
if (result == null || result == DBNull.Value)
|
||||
{
|
||||
logger.Error("Kafka入库心跳ts_ms查询结果为空");
|
||||
Logger.Error("Kafka入库心跳ts_ms查询结果为空");
|
||||
ExecuteKafkaDbConnectionAlert();
|
||||
return;
|
||||
}
|
||||
|
||||
if (!long.TryParse(result.ToString(), out long lastTsMs))
|
||||
// 解析时间戳(毫秒)
|
||||
if (!long.TryParse(result.ToString(), out var lastTsMs))
|
||||
{
|
||||
logger.Error("Kafka入库心跳ts_ms解析失败");
|
||||
Logger.Error("Kafka入库心跳ts_ms解析失败");
|
||||
ExecuteKafkaDbConnectionAlert();
|
||||
return;
|
||||
}
|
||||
|
||||
long nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
if (nowMs - lastTsMs > TimeSpan.FromMinutes(5).TotalMilliseconds)
|
||||
// 按“当前时间 - 最新入库时间”判断是否超时
|
||||
var nowMs = DateTimeOffset.UtcNow.ToUnixTimeMilliseconds();
|
||||
if (nowMs - lastTsMs > TimeSpan.FromMinutes(KafkaStaleMinutes).TotalMilliseconds)
|
||||
{
|
||||
logger.Error("Kafka入库心跳超过5分钟未更新");
|
||||
Logger.Error($"Kafka入库心跳超过{KafkaStaleMinutes}分钟未更新");
|
||||
ExecuteKafkaInactiveAlert();
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
logger.Error($"Kafka入库心跳检查错误: {ex.Message}");
|
||||
Logger.Error($"Kafka入库心跳检查错误: {ex.Message}");
|
||||
ExecuteKafkaDbConnectionAlert();
|
||||
}
|
||||
await Task.CompletedTask;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 生成PostgreSQL连接字符串
|
||||
/// 从配置读取 Postgres 参数并生成连接字符串。
|
||||
/// </summary>
|
||||
/// <returns>可用连接串;若关键配置缺失则返回 null</returns>
|
||||
private string? BuildPostgresConnectionString()
|
||||
{
|
||||
IConfigurationSection section = _configuration.GetSection("Postgres");
|
||||
string? host = section["Host"];
|
||||
string? portString = section["Port"];
|
||||
string? database = section["Database"];
|
||||
string? username = section["User"];
|
||||
string? password = section["Password"];
|
||||
string? maxConnectionsString = section["MaxConnections"];
|
||||
string? idleTimeoutMsString = section["IdleTimeoutMs"];
|
||||
// 约定配置节点:Postgres
|
||||
var section = _configuration.GetSection("Postgres");
|
||||
|
||||
var host = section["Host"];
|
||||
var portString = section["Port"];
|
||||
var database = section["Database"];
|
||||
var username = section["User"];
|
||||
var password = section["Password"];
|
||||
var maxConnectionsString = section["MaxConnections"];
|
||||
var idleTimeoutMsString = section["IdleTimeoutMs"];
|
||||
|
||||
// 必填项校验
|
||||
if (string.IsNullOrWhiteSpace(host) ||
|
||||
string.IsNullOrWhiteSpace(portString) ||
|
||||
string.IsNullOrWhiteSpace(database) ||
|
||||
@@ -512,11 +585,13 @@ namespace AutoNotificatPhone.Models
|
||||
return null;
|
||||
}
|
||||
|
||||
if (!int.TryParse(portString, out int port))
|
||||
// 端口格式校验
|
||||
if (!int.TryParse(portString, out var port))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
// 构建基础连接串
|
||||
var builder = new NpgsqlConnectionStringBuilder
|
||||
{
|
||||
Host = host,
|
||||
@@ -526,12 +601,14 @@ namespace AutoNotificatPhone.Models
|
||||
Password = password
|
||||
};
|
||||
|
||||
if (int.TryParse(maxConnectionsString, out int maxConnections) && maxConnections > 0)
|
||||
// 连接池最大连接数(可选)
|
||||
if (int.TryParse(maxConnectionsString, out var maxConnections) && maxConnections > 0)
|
||||
{
|
||||
builder.MaxPoolSize = maxConnections;
|
||||
}
|
||||
|
||||
if (int.TryParse(idleTimeoutMsString, out int idleTimeoutMs) && idleTimeoutMs > 0)
|
||||
// 空闲连接生命周期(ms -> s)
|
||||
if (int.TryParse(idleTimeoutMsString, out var idleTimeoutMs) && idleTimeoutMs > 0)
|
||||
{
|
||||
builder.ConnectionIdleLifetime = Math.Max(1, idleTimeoutMs / 1000);
|
||||
}
|
||||
@@ -540,7 +617,7 @@ namespace AutoNotificatPhone.Models
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行Kafka入库失活警报
|
||||
/// Kafka 入库停滞告警。
|
||||
/// </summary>
|
||||
private void ExecuteKafkaInactiveAlert()
|
||||
{
|
||||
@@ -548,98 +625,118 @@ namespace AutoNotificatPhone.Models
|
||||
smsContent: "[BLV运维提示] BLS数据库3分钟内入库数据为0。",
|
||||
callContent: "BLV运维提示 BLS数据库3分钟内入库数据为0",
|
||||
alertType: "BLS-数据库入库警报",
|
||||
extendedDeadline: true
|
||||
);
|
||||
extendedDeadline: true);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 执行Kafka入库数据库连接失败警报
|
||||
/// Kafka 数据库连接异常告警(带计数节流)。
|
||||
/// </summary>
|
||||
private void ExecuteKafkaDbConnectionAlert()
|
||||
{
|
||||
// 每次失败计数 +1,累计到阈值再告警
|
||||
_kafkaDbConnectionAlertCount++;
|
||||
if (_kafkaDbConnectionAlertCount < 8)
|
||||
if (_kafkaDbConnectionAlertCount < KafkaDbAlertTriggerCount)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// 触发一次后清零重新计数
|
||||
SendAlert(
|
||||
smsContent: "[BLV运维提示] 数据库连接失败!",
|
||||
callContent: "[BLV运维提示] 数据库连接失败",
|
||||
alertType: "BLS-数据库连接警报",
|
||||
extendedDeadline: true
|
||||
);
|
||||
extendedDeadline: true);
|
||||
|
||||
_kafkaDbConnectionAlertCount = 0;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 解析CPU值字符串为整数列表
|
||||
/// 将逗号分隔字符串解析为整型列表;解析失败项按 0 处理。
|
||||
/// </summary>
|
||||
private List<int> ParseCpuValues(string valueString)
|
||||
/// <param name="valueString">CSV 字符串</param>
|
||||
/// <returns>整型列表</returns>
|
||||
private static List<int> ParseCsvToIntList(string valueString)
|
||||
{
|
||||
return string.IsNullOrEmpty(valueString)
|
||||
? new List<int>()
|
||||
: valueString.Split(',').Select(v => int.TryParse(v, out int result) ? result : 0).ToList();
|
||||
if (string.IsNullOrEmpty(valueString))
|
||||
{
|
||||
return [];
|
||||
}
|
||||
|
||||
return valueString
|
||||
.Split(',')
|
||||
.Select(item => int.TryParse(item, out var number) ? number : 0)
|
||||
.ToList();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 检查值列表是否超过阈值
|
||||
/// 判断列表中是否至少有 requiredCount 个值达到 threshold。
|
||||
/// </summary>
|
||||
private bool CheckThreshold(List<int> values, int threshold, int requiredCount)
|
||||
/// <param name="values">待检查值集合</param>
|
||||
/// <param name="threshold">阈值</param>
|
||||
/// <param name="requiredCount">最少命中数量</param>
|
||||
/// <returns>满足返回 true</returns>
|
||||
private static bool CheckThreshold(List<int> values, int threshold, int requiredCount)
|
||||
{
|
||||
return values != null &&
|
||||
values.Count >= requiredCount &&
|
||||
values.Count(v => v >= threshold) >= requiredCount;
|
||||
return values.Count >= requiredCount && values.Count(v => v >= threshold) >= requiredCount;
|
||||
}
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// 通用警报发送方法
|
||||
/// 统一告警发送入口(两条短信 + 一通电话)。
|
||||
/// </summary>
|
||||
/// <param name="smsContent">短信内容</param>
|
||||
/// <param name="callContent">电话播报内容</param>
|
||||
/// <param name="alertType">告警类型(用于 caller 与日志)</param>
|
||||
/// <param name="extendedDeadline">是否使用扩展过期时间</param>
|
||||
private void SendAlert(string smsContent, string callContent, string alertType, bool extendedDeadline = false)
|
||||
{
|
||||
// 设置过期时间
|
||||
long currentTimestamp = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
long smsDeadline = extendedDeadline ? currentTimestamp + 3600 : currentTimestamp + 1800;
|
||||
long callDeadline = extendedDeadline ? currentTimestamp + 1800 : currentTimestamp + 900;
|
||||
// 计算任务过期时间
|
||||
var nowSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
var smsDeadline = nowSeconds + (extendedDeadline ? ExtendedSmsDeadlineSeconds : SmsDeadlineSeconds);
|
||||
var callDeadline = nowSeconds + (extendedDeadline ? ExtendedCallDeadlineSeconds : CallDeadlineSeconds);
|
||||
|
||||
// 创建请求
|
||||
var smsRequest = CreateSmsRequest("2", smsDeadline, Mobile1, alertType, smsContent);
|
||||
// 构建请求:两路短信(Mobile1/2)+ 一路电话(Mobile1)
|
||||
var smsRequest1 = CreateSmsRequest("2", smsDeadline, Mobile1, alertType, smsContent);
|
||||
var smsRequest2 = CreateSmsRequest("2", smsDeadline, Mobile2, alertType, smsContent);
|
||||
var callRequest = CreateSmsRequest("1", callDeadline, Mobile1, alertType, callContent);
|
||||
|
||||
// 发送警报
|
||||
var smsResult = _callAndMsgController.SendToPhone(smsRequest);
|
||||
// 调用 API 投递任务
|
||||
var smsResult1 = _callAndMsgController.SendToPhone(smsRequest1);
|
||||
var smsResult2 = _callAndMsgController.SendToPhone(smsRequest2);
|
||||
var callResult = _callAndMsgController.SendToPhone(callRequest);
|
||||
|
||||
// 记录结果
|
||||
if (smsResult.isok && callResult.isok)
|
||||
// 任意一路失败都记录错误日志
|
||||
if (!smsResult1.isok || !smsResult2.isok || !callResult.isok)
|
||||
{
|
||||
//logger.Error($"{alertType}警告已发送");
|
||||
}
|
||||
else
|
||||
{
|
||||
logger.Error($"发送{alertType}通知失败: 短信={smsResult.message} 电话={callResult.message}");
|
||||
Logger.Error($"发送{alertType}通知失败: 短信1={smsResult1.message} 短信2={smsResult2.message} 电话={callResult.message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 创建短信/电话请求对象
|
||||
/// 创建短信/电话请求对象。
|
||||
/// </summary>
|
||||
private SmsRequest CreateSmsRequest(string type, long deadline, string phone, string caller, string content)
|
||||
/// <param name="type">1=电话,2=短信</param>
|
||||
/// <param name="deadline">截止时间(Unix 秒)</param>
|
||||
/// <param name="phone">目标手机号</param>
|
||||
/// <param name="caller">任务标识/来电名称</param>
|
||||
/// <param name="content">内容</param>
|
||||
/// <returns>SmsRequest 对象</returns>
|
||||
private static SmsRequest CreateSmsRequest(string type, long deadline, string phone, string caller, string content)
|
||||
{
|
||||
return new SmsRequest
|
||||
{
|
||||
// 业务类型(1 电话 / 2 短信)
|
||||
Type = type,
|
||||
// 截止时间
|
||||
DeadLine = deadline,
|
||||
// 创建时间
|
||||
StartingPoint = DateTimeOffset.UtcNow.ToUnixTimeSeconds(),
|
||||
// 目标号码
|
||||
PhoneNumber = phone,
|
||||
// 任务标识
|
||||
CallerName = caller,
|
||||
// 消息内容
|
||||
Content = content
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
357
README.md
Normal file
357
README.md
Normal file
@@ -0,0 +1,357 @@
|
||||
# Web_AutoNotificatPhone_Server_Prod
|
||||
|
||||
## 1. 项目概述
|
||||
|
||||
这是一个基于 **ASP.NET Core 8 MVC** 的通知与巡检服务,核心职责是:
|
||||
|
||||
1. 提供 HTTP API,接收“电话/短信”通知请求并发布到 Redis 频道。
|
||||
2. 运行后台定时任务(`TimerClass`),每分钟做系统状态检查。
|
||||
3. 根据 Redis 指标和 PostgreSQL 心跳数据触发自动告警(短信 + 电话)。
|
||||
|
||||
项目本质上是一个“**告警编排层**”:
|
||||
- 输入来源:HTTP 请求、Redis 指标、PostgreSQL 心跳表。
|
||||
- 输出目标:Redis 发布(供下游呼叫/短信系统消费)。
|
||||
|
||||
---
|
||||
|
||||
## 2. 技术栈与依赖
|
||||
|
||||
- .NET: `net8.0`
|
||||
- Web: ASP.NET Core MVC + API Controller
|
||||
- Redis: `CSRedisCore`
|
||||
- PostgreSQL: `Npgsql`
|
||||
- Logging: `NLog`
|
||||
|
||||
`AutoNotificatPhone.csproj` 关键包:
|
||||
- `CSRedisCore`
|
||||
- `NLog`
|
||||
- `Npgsql`
|
||||
|
||||
---
|
||||
|
||||
## 3. 目录与核心文件
|
||||
|
||||
- `Program.cs`:应用启动、CORS、路由、HostedService 注册。
|
||||
- `Controllers/CallAndMsgController.cs`:通知入站 API(电话/短信)。
|
||||
- `Models/TimerClass.cs`:后台巡检与告警主流程。
|
||||
- `CSRedisCacheHelper.cs`:Redis 连接与基础操作封装。
|
||||
- `Models/SmsRequest.cs`:通知请求模型。
|
||||
- `Models/ReturnInfo.cs`:统一接口返回结构。
|
||||
- `appsettings.json`:日志级别、Postgres 连接配置。
|
||||
- `nlog.config`:日志落盘规则。
|
||||
|
||||
---
|
||||
|
||||
## 4. 启动流程(程序生命周期)
|
||||
|
||||
### 4.1 Program 启动阶段
|
||||
|
||||
`Program.cs` 主要行为:
|
||||
|
||||
1. `AddControllersWithViews()`:注册 MVC 和 API 支持。
|
||||
2. 注册 CORS 策略 `AllowAnyOrigin`:
|
||||
- 允许任意源
|
||||
- 允许任意方法
|
||||
- 允许任意请求头
|
||||
3. `AddHostedService<TimerClass>()`:注册后台巡检服务。
|
||||
4. 管道顺序:
|
||||
- `UseStaticFiles()`
|
||||
- `UseRouting()`
|
||||
- `UseCors("AllowAnyOrigin")`
|
||||
- `UseAuthorization()`
|
||||
- `MapControllerRoute(...)`
|
||||
|
||||
### 4.2 后台服务启动
|
||||
|
||||
应用启动后,`TimerClass` 自动运行:
|
||||
- 持续循环,直到应用停止。
|
||||
- 每分钟固定在第 30 秒执行一次巡检逻辑。
|
||||
|
||||
---
|
||||
|
||||
## 5. API 处理流程(CallAndMsgController)
|
||||
|
||||
## 5.1 路由
|
||||
|
||||
Controller 路由前缀:
|
||||
- `api/CallAndMsg/{action}`
|
||||
|
||||
核心接口:
|
||||
- `POST api/CallAndMsg/SendToPhone`
|
||||
|
||||
## 5.2 请求模型(SmsRequest)
|
||||
|
||||
- `PhoneNumber`:目标手机号
|
||||
- `CallerName`:任务标识/来电显示名
|
||||
- `Content`:消息内容
|
||||
- `StartingPoint`:开始时间(Unix 秒)
|
||||
- `DeadLine`:截止时间(Unix 秒)
|
||||
- `Type`:`1` 电话,`2` 短信
|
||||
|
||||
## 5.3 接口执行逻辑
|
||||
|
||||
`SendToPhone` 内部流程:
|
||||
|
||||
1. 接收 `SmsRequest`。
|
||||
2. 组装 `notification` 对象。
|
||||
3. 使用固定频道 `MsgAndCall` 发布到 Redis:
|
||||
- `CSRedisCacheHelper.Publish("MsgAndCall", payload)`
|
||||
4. 返回 `ReturnInfo`:
|
||||
- 成功:`status=200` + 根据 `Type` 返回“已加入队列”文案
|
||||
- 失败:`status=500` + 异常信息
|
||||
|
||||
> 说明:`ConvertTimestampToIso` 名称为“转 ISO”,但实际仍返回 Unix 秒。
|
||||
|
||||
---
|
||||
|
||||
## 6. Redis 封装(CSRedisCacheHelper)
|
||||
|
||||
## 6.1 连接
|
||||
|
||||
静态初始化时连接本地 Redis:
|
||||
- Host: `127.0.0.1`
|
||||
- Port: `10079`
|
||||
- 密码:代码中硬编码
|
||||
- DB0: `redis`
|
||||
- DB1: `redis1`
|
||||
|
||||
## 6.2 用途
|
||||
|
||||
- `DB0`:主要用于发布通知频道(`MsgAndCall`)。
|
||||
- `DB1`:主要用于读取巡检指标(CPU、在线数、收发包等)。
|
||||
|
||||
## 6.3 方法
|
||||
|
||||
- `Set/Get/Forever/Del/Contains`
|
||||
- `Publish`
|
||||
|
||||
---
|
||||
|
||||
## 7. 核心:TimerClass 业务流程详解
|
||||
|
||||
`TimerClass` 是项目核心,分为 **调度层**、**检查层**、**告警层**。
|
||||
|
||||
## 7.1 调度层(每分钟触发)
|
||||
|
||||
主循环执行顺序:
|
||||
|
||||
1. 计算下次执行时间(每分钟第 30 秒)。
|
||||
2. 检查电话机进程是否在线(进程名 `Telephone`)。
|
||||
3. 运行整点任务调度。
|
||||
4. 依次执行检查任务:
|
||||
- CPU 阈值
|
||||
- RCU 在线数量
|
||||
- 总发送包
|
||||
- 总接收包
|
||||
- PostgreSQL 心跳
|
||||
|
||||
出现异常时:
|
||||
- 记录日志
|
||||
- 延迟 10 秒重试
|
||||
|
||||
## 7.2 整点任务调度
|
||||
|
||||
方法:`RunHourlyNotificationTask()`
|
||||
|
||||
规则:
|
||||
- 使用北京时间(UTC+8)。
|
||||
- 非整点(`Minute != 0`)不执行。
|
||||
- 同一整点只执行一次(`_executedTasks` 去重)。
|
||||
- 10/15/22 点执行每日任务,其余整点发整点短信。
|
||||
|
||||
### 7.2.1 整点短信(SendHourlySms)
|
||||
|
||||
- 给 `Mobile1` 发送一条短信任务。
|
||||
- 文案包含当前“月/日/时”。
|
||||
|
||||
### 7.2.2 每日任务(ExecuteDailyTask)
|
||||
|
||||
- 给 `Mobile1` 和 `Mobile2` 发送短信。
|
||||
- 给 `Mobile1` 发送电话任务。
|
||||
- 任意发送失败记录错误日志。
|
||||
|
||||
## 7.3 Redis 指标检查
|
||||
|
||||
### 7.3.1 CPU 检查(CheckCpuThreshold)
|
||||
|
||||
步骤:
|
||||
1. 读取 `UDPPackage_DetectTime`:
|
||||
- 若超过 10 分钟未更新,触发“监控程序无法访问”告警。
|
||||
2. 读取:
|
||||
- `UDPPackage_CPUMax`
|
||||
- `UDPPackage_CPUAvg`
|
||||
- `UDPPackage_CPUMin`
|
||||
3. 若 `CPUAvg` 中满足“>=80 的点数达到 6 个”,触发 CPU 告警。
|
||||
|
||||
### 7.3.2 通用 Redis 规则(CheckRedisValue)
|
||||
|
||||
用于在线数/发送包/接收包的基线跌落判断:
|
||||
|
||||
1. 读取 CSV 序列。
|
||||
2. 取前 `baselineCount` 个点做平均。
|
||||
3. 阈值 = 平均值 × `thresholdRatio`。
|
||||
4. 若后续两个点都低于阈值,触发告警。
|
||||
|
||||
具体调用:
|
||||
- `RCUOnLine`:`baselineCount=8`,`ratio=0.8`
|
||||
- `UDPPackage_TotalSendPackage`:`baselineCount=8`,`ratio=0.6`
|
||||
- `UDPPackage_TotalRecvPackage`:`baselineCount=8`,`ratio=0.75`
|
||||
|
||||
### 7.3.3 接收包特殊规则(CheckTotalRecvPackage)
|
||||
|
||||
在通用规则之前,先判断:
|
||||
- 若最后 3 个值都 `< 70000`,立即触发告警。
|
||||
|
||||
## 7.4 PostgreSQL 心跳检查
|
||||
|
||||
方法:`CheckKafkaHeartbeatAsync()`
|
||||
|
||||
目标:判断“Kafka 入库是否活跃”。
|
||||
|
||||
流程:
|
||||
1. 从 `appsettings.json` 的 `Postgres` 节生成连接串。
|
||||
2. 查询 `heartbeat.heartbeat_events`:
|
||||
- 先按 `ts_ms desc` 取最近 3000 条
|
||||
- 再按 `write_ts_ms desc` 取最新一条
|
||||
3. 结果为空或解析失败:走“数据库连接失败”告警路径。
|
||||
4. 若 `nowMs - lastTsMs > KafkaStaleMinutes`:触发“入库停滞”告警。
|
||||
5. 异常(连接失败/SQL 异常)同样走“数据库连接失败”告警。
|
||||
|
||||
### 7.4.1 数据库连接失败节流
|
||||
|
||||
- 字段:`_kafkaDbConnectionAlertCount`
|
||||
- 逻辑:失败累计到 `KafkaDbAlertTriggerCount=8` 才真正发一次告警,之后计数清零。
|
||||
|
||||
---
|
||||
|
||||
## 8. 告警发送机制
|
||||
|
||||
统一通过 `SendAlert(...)`:
|
||||
|
||||
1. 计算过期时间:
|
||||
- 普通:短信 1800 秒、电话 900 秒
|
||||
- 扩展:短信 3600 秒、电话 1800 秒
|
||||
2. 构造 3 条任务:
|
||||
- 短信:`Mobile1`
|
||||
- 短信:`Mobile2`
|
||||
- 电话:`Mobile1`
|
||||
3. 调用 `CallAndMsgController.SendToPhone` 投递。
|
||||
4. 任一失败,记录错误日志。
|
||||
|
||||
---
|
||||
|
||||
## 9. 配置说明
|
||||
|
||||
## 9.1 appsettings.json
|
||||
|
||||
当前包含:
|
||||
|
||||
- `Logging`
|
||||
- `AllowedHosts`
|
||||
- `Postgres`
|
||||
- `Host`
|
||||
- `Port`
|
||||
- `Database`
|
||||
- `User`
|
||||
- `Password`
|
||||
- `MaxConnections`
|
||||
- `IdleTimeoutMs`
|
||||
|
||||
## 9.2 nlog.config
|
||||
|
||||
- 文件落盘目录:`{basedir}/Logs/{shortdate}`
|
||||
- 文件:`info_*.txt`、`error_*.txt`
|
||||
- 当前规则:
|
||||
- `FATAL` 写入 info 文件
|
||||
- `Error` 及以上写入 error 文件
|
||||
|
||||
---
|
||||
|
||||
## 10. 快速运行
|
||||
|
||||
## 10.1 开发运行
|
||||
|
||||
```bash
|
||||
dotnet restore
|
||||
dotnet build AutoNotificatPhone.csproj
|
||||
dotnet run --project AutoNotificatPhone.csproj
|
||||
```
|
||||
|
||||
默认开发地址见 `Properties/launchSettings.json`:
|
||||
- `http://localhost:5080`
|
||||
|
||||
## 10.2 VS Code Tasks
|
||||
|
||||
项目已配置任务:
|
||||
- `build`
|
||||
- `publish`
|
||||
- `watch`
|
||||
|
||||
---
|
||||
|
||||
## 11. 对外接口示例
|
||||
|
||||
请求:
|
||||
|
||||
```json
|
||||
POST /api/CallAndMsg/SendToPhone
|
||||
Content-Type: application/json
|
||||
|
||||
{
|
||||
"PhoneNumber": "13500000000",
|
||||
"CallerName": "测试任务",
|
||||
"Content": "测试短信内容",
|
||||
"StartingPoint": 1760000000,
|
||||
"DeadLine": 1760001800,
|
||||
"Type": "2"
|
||||
}
|
||||
```
|
||||
|
||||
成功响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"isok": true,
|
||||
"message": "短信已加入发送队列",
|
||||
"status": 200,
|
||||
"response": "success"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. 关键数据流(一句话版)
|
||||
|
||||
外部请求/定时巡检 -> 生成 `SmsRequest` -> `CallAndMsgController` -> Redis 频道 `MsgAndCall` -> 下游系统消费执行。
|
||||
|
||||
---
|
||||
|
||||
## 13. 当前实现中的注意点
|
||||
|
||||
1. `CallAndMsgController` 中 `ConvertTimestampToIso` 命名与实现含义不一致(返回 Unix 秒)。
|
||||
2. `CSRedisCacheHelper` 中 Redis 密码硬编码在代码,不利于安全与环境切换。
|
||||
3. `TimerClass` 的 `KafkaStaleMinutes` 与告警文案“3分钟内入库数据为0”存在语义不一致风险(建议统一)。
|
||||
4. 当前 CORS 为完全放开,生产环境建议收敛来源。
|
||||
|
||||
---
|
||||
|
||||
## 14. 后续可优化方向(可选)
|
||||
|
||||
1. 将手机号、阈值、执行小时、告警文案全部配置化。
|
||||
2. 让 `TimerClass` 通过接口服务发送通知,避免直接 new Controller。
|
||||
3. 增加单元测试:
|
||||
- Redis 阈值计算
|
||||
- Kafka 心跳判定
|
||||
- 告警节流逻辑
|
||||
4. 增加健康检查端点(数据库/Redis 连通性)。
|
||||
|
||||
---
|
||||
|
||||
## 15. 维护建议
|
||||
|
||||
每次修改巡检规则后,至少验证:
|
||||
|
||||
1. `dotnet build` 无编译错误。
|
||||
2. Redis 有模拟数据时告警触发正确。
|
||||
3. PostgreSQL 心跳超时/失败路径可复现。
|
||||
4. `MsgAndCall` 频道消息格式未破坏下游兼容。
|
||||
Reference in New Issue
Block a user