track/src/SJZY.Track.Application/Services/ForewarningServices/ForewarningNotifyService.cs
2024-09-18 10:38:32 +08:00

349 lines
13 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

using System.Text;
using FreeRedis;
using SJZY.Expand.ABP.Core;
using SJZY.Expand.ABP.Core.Logger;
using SJZY.Track.Application.Contract.Dtos.Forewarning;
using SJZY.Track.Application.Contract.Services;
using SJZY.Track.Domain.Aggregates.ForewarningAggregate;
using SJZY.Track.Domain.Dtos.Forewarning;
using SJZY.Track.Domain.Queries;
using SJZY.Track.Share.Enums;
using SJZY.UserCenter.HttpApi.Services.Message;
using SJZY.UserCenter.SharedModel.Dtos.Message;
using SJZY.UserCenter.SharedModel.Enums;
using Volo.Abp.DependencyInjection;
using Volo.Abp.Http.Client.DynamicProxying;
namespace SJZY.Track.Application.Services.ForewarningServices;
/// <summary>
/// 用于处理预警通知
/// </summary>
public class ForewarningNotifyService : IForewarningNotifyService, ITransientDependency
{
private readonly IForewarningCacheService _forewarningCacheService;
private readonly IHttpClientProxy<IRequestSendMessageService> _requestSendMessageService;
private readonly IWarningQueries _warningQueries;
private readonly IWarningRecordEsRepository _warningRecordEsRepository;
public ForewarningNotifyService(IWarningQueries warningQueries,
IWarningRecordEsRepository warningRecordEsRepository,
IHttpClientProxy<IRequestSendMessageService> requestSendMessageService,
IForewarningCacheService forewarningCacheService)
{
_warningQueries = warningQueries;
_warningRecordEsRepository = warningRecordEsRepository;
_requestSendMessageService = requestSendMessageService;
_forewarningCacheService = forewarningCacheService;
}
/// <summary>
/// 创建预警通知
/// </summary>
/// <param name="dto">创建预警通知需要的订单信息</param>
public async Task WarningCalculateAsync(WarningDto dto)
{
if (dto.Orders.Count == 0) return;
var rules = await _forewarningCacheService.GetRulesAsync(dto.SubSystemId, dto.CustomerId);
if (rules == null || rules.Count == 0) return;
var adds = new List<(int ruleId, int orderId, int counter, long ts)>();
var dels = new List<(int ruleId, int orderId, int counter)>();
foreach (var order in dto.Orders)
{
// 加左区间
MatchRule(
rules.Where(rule => rule.StartTrackStatusId == dto.TrackStatusId).ToList(),
order
).ForEach(rule =>
{
var interval = (rule.TotalTimeSpan - rule.WarnTimeSpan) / rule.WarnCount;
var startTime = order.TrackTime.AddMinutes(rule.WarnTimeSpan);
for (var i = 0; i < rule.WarnCount; i++)
{
var timestamp = GetTimestamp(startTime);
startTime = startTime.AddMinutes(interval);
// 计算预警时间戳,作为排序依据
adds.Add((rule.Id, order.Id, i, timestamp));
}
});
// 删右区间
MatchRule(
rules.Where(rule => rule.EndTrackStatusId == dto.TrackStatusId).ToList(),
order
).ForEach(rule =>
{
for (var i = 0; i < rule.WarnCount; i++)
{
dels.Add((rule.Id, order.Id, i));
}
});
}
var redisKey = GetForewarningNotifySetKeyRedisKey(dto.SubSystemId);
long addCounter, delCounter = 0;
var tran = RedisHelper.Client.Multi();
// 左区间加入预警队列,【只有第一次发生的才会被加入】
addCounter = await tran.ZAddNxAsync(redisKey, adds.Select(x =>
{
var entry = new WarningNotifyCacheEntry(x.ruleId, x.orderId, x.counter);
return new ZMember(entry.ToString(), x.ts);
}).ToArray());
// 右区间移出预警队列,【只要发生了就移除】
var right = dels.Select(x => new WarningNotifyCacheEntry(x.ruleId, x.orderId, x.counter)).ToList();
if (!right.IsNullOrEmpty())
{
delCounter = await tran.ZRemAsync(redisKey, right.Select(x => x.ToString()).ToArray());
}
tran.Exec();
LogHelper.Info($"本次共计产生{addCounter}条预警,移除{delCounter}条预警,触发缘由是");
await _warningRecordEsRepository.BulkUpdateAsync(dels); // 标记处理完成
}
public async Task<int> SendNotificationAsync(int subSystemId)
{
var redisKey = GetForewarningNotifySetKeyRedisKey(subSystemId);
var timestamp = GetTimestamp(DateTime.Now);
var set = await RedisHelper.Client.ZRangeByScoreAsync(redisKey, 0, timestamp);
if (set.IsNullOrEmpty())
{
LogHelper.Info("没有运输时效需要预警");
return 0;
}
var entries = set.Select(x => new WarningNotifyCacheEntry(x)).ToList();
var ruleIds = entries.Select(x => x.RuleId).Distinct().ToList();
var notifyRules = await _warningQueries.GetWarningNotifyRulesAsync(ruleIds);
// 写入es预警记录
var warningRecords = await WriteWarningRecordAsync(entries, notifyRules);
// 发通知
await SendCoreAsync(subSystemId, entries, notifyRules);
// 移除已处理的预警
await RedisHelper.Client.ZRemRangeByScoreAsync(redisKey, 0, timestamp);
return warningRecords.Count;
}
private async Task<List<WarningRecordIndex>> WriteWarningRecordAsync(List<WarningNotifyCacheEntry> entries,
List<GetWarningNotifiesVo> notifyRules)
{
var warningRecords = new List<WarningRecordIndex>();
foreach (var entry in entries)
{
var rule = notifyRules.FirstOrDefault(x => x.RuleId == entry.RuleId);
if (rule == null) continue;
warningRecords.Add(new WarningRecordIndex
{
SubSystemId = rule.SubSystemId,
WarningName = rule.WarningName,
CustomName = rule.CustomName,
RuleId = entry.RuleId,
Rule = rule.ToString(),
Counter = entry.Counter,
OrderId = entry.OrderId,
WarningStatus = WarningStatus.UnProcessed,
NotifyUser = rule.NotifyUser,
CreateTime = DateTime.Now,
});
}
await _warningRecordEsRepository.BulkInsertAsync(warningRecords);
return warningRecords;
}
private Task SendCoreAsync(int subSystemId, List<WarningNotifyCacheEntry> entries,
List<GetWarningNotifiesVo> notifyRules)
{
var userRules = new Dictionary<int, List<GetWarningNotifiesVo>>();
foreach (var rule in notifyRules)
{
foreach (var userId in rule.NotifyUserIds)
{
if (!userRules.ContainsKey(userId))
{
userRules[userId] = new List<GetWarningNotifiesVo>();
}
userRules[userId].Add(rule);
}
}
foreach (var userRule in userRules)
{
// 以人的维度发送通知,异步
_ = DoNotifyAsync(subSystemId, userRule.Key, userRule.Value, entries,
NotifyMode.SiteInternal);
_ = DoNotifyAsync(subSystemId, userRule.Key, userRule.Value, entries, NotifyMode.FeiShu);
}
return Task.CompletedTask;
}
private string GetForewarningNotifySetKeyRedisKey(int subSystemId) =>
$"{GlobalConst.ForewarningNotifySetKey}:{subSystemId}";
private async Task DoNotifyAsync(int subSystemId, int userId, List<GetWarningNotifiesVo> rules,
List<WarningNotifyCacheEntry> entries,
NotifyMode mode)
{
/* 以人的维度发送通知
*
预警名称:字节全程时效
预警规则:
{起飞-落地+普货+12345678+美国 时效9,预警时效7,次数3} 命中订单总数:124,详情点击https://middlebusiness.pet.shijizhongyun.com/trajectoryCenter/trajectoryOrder
{清关-提货+普货+12345678+美国 时效4,预警时效3,次数1} 命中订单总数:3250,详情点击https://middlebusiness.pet.shijizhongyun.com/trajectoryCenter/trajectoryOrder
预警名称:字节全程时效
预警规则:
{起飞-落地+普货+12345678+美国 时效9,预警时效7,次数3} 命中订单总数:124,详情点击https://middlebusiness.pet.shijizhongyun.com/trajectoryCenter/trajectoryOrder
{清关-提货+普货+12345678+美国 时效4,预警时效3,次数1} 命中订单总数:3250,详情点击https://middlebusiness.pet.shijizhongyun.com/trajectoryCenter/trajectoryOrder
*/
var notifyContent = new StringBuilder();
foreach (var warningGroup in rules.Where(x => x.NotifyModes.Contains(mode))
.GroupBy(x => x.WarningName))
{
notifyContent.AppendLine($"预警名称:{warningGroup.Key}");
notifyContent.AppendLine($"预警规则:");
foreach (var rule in warningGroup.ToList())
{
var cnt = entries.Count(x => x.RuleId == rule.RuleId);
notifyContent.AppendLine(
$"{rule.ToString()} 命中订单总数:{cnt},详情点击https://middlebusiness.pet.shijizhongyun.com/trajectoryCenter/trajectoryOrder");
}
}
var msgContent = notifyContent.ToString();
if (string.IsNullOrEmpty(msgContent)) return;
var res = await _requestSendMessageService.Service.SendMessageByEmptyTemplateAsync(
new SendMessageByEmptyTemplateInput
{
IsNeedToHandle = true,
MsgSource = MsgSourceEnum.系统提醒,
SystemId = subSystemId,
TemplateCode = "trackWarning",
Channels = new List<ChannelEnum>()
{
mode == NotifyMode.FeiShu ? ChannelEnum.飞书 : ChannelEnum.站内信,
},
MsgTitle = "轨迹运输时效预警",
MsgContent = msgContent,
receivers = new List<MsgInstanceReceiverInput>
{
new()
{
RecipientType = 1,
ReceiverId = userId,
}
},
});
if (res.Code != GlobalConst.SuccessCode)
{
LogHelper.Error($"发送{mode.ToString()}通知失败,用户:{userId},内容:{msgContent}");
}
}
private static List<ForewarningRuleCacheEntry> MatchRule(List<ForewarningRuleCacheEntry> rules,
WarningOrderDto order)
{
var res = new List<ForewarningRuleCacheEntry>();
foreach (var rule in rules)
{
// 规则至少有一个因素,如果订单任一因素不符合则不命中
if (rule.IsTransit != null)
{
if (order.IsTransit != rule.IsTransit) continue;
}
if (!rule.CargoType.IsNullOrEmpty())
{
if (order.CargoType != rule.CargoType) continue;
}
if (!rule.LeavePort.IsNullOrEmpty())
{
if (order.LeavePort != rule.LeavePort) continue;
}
if (!rule.ProductCode.IsNullOrEmpty())
{
if (order.ProductCode != rule.ProductCode) continue;
}
if (!rule.Months.IsNullOrEmpty())
{
if (!order.Month.IsIn(rule.Months)) continue;
}
if (!rule.Areas.IsNullOrEmpty())
{
var inArea = false;
var areas = rule.Areas!.Where(area => order.ArrivalCountry == area.Country).ToList();
foreach (var area in areas)
{
if (!area.LeftPostcode.IsNullOrEmpty() && !area.RightPostcode.IsNullOrEmpty())
{
// tips: 参考小包的邮编匹配,字典序
if (string.CompareOrdinal(order.ArrivalPostCode, area.LeftPostcode) < 0 ||
string.CompareOrdinal(order.ArrivalPostCode, area.RightPostcode) > 0) continue;
}
inArea = true;
break;
}
if (!inArea)
{
foreach (var area in areas)
{
if (!area.City.IsNullOrEmpty() && order.ArrivalCity != area.City) continue;
inArea = true;
break;
}
}
if (!inArea)
{
foreach (var area in areas)
{
if (!area.Province.IsNullOrEmpty() && order.ArrivalProvince != area.Province) continue;
inArea = true;
break;
}
}
if (!inArea) continue;
}
// 命中规则
res.Add(rule);
}
return res;
}
private static long GetTimestamp(DateTime dateTime)
{
return new DateTimeOffset(dateTime).ToUnixTimeSeconds();
}
}