349 lines
13 KiB
C#
349 lines
13 KiB
C#
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();
|
||
}
|
||
}
|