Merge branch 'S07' into test

# Conflicts:
#	src/SJZY.Track.Application.Contract/SJZY.Track.Application.Contract.csproj
This commit is contained in:
hardstylewyl 2025-06-06 10:07:52 +08:00
commit 04d7cf1860
25 changed files with 613 additions and 489 deletions

View File

@ -2,21 +2,10 @@ using SJZY.Expand.ABP.Core.Configuration;
using SJZY.Expand.ABP.Core.Configuration.AutoRegistOptions;
using SJZY.Expand.ABP.Core.Logger;
using SJZY.Track.Api;
using SJZY.Track.Application;
try
{
var builder = WebApplication.CreateBuilder(args);
if (args.Length > 0)
{
_BUGOptions.EnablePref = "on".Equals(args[0]);
Console.WriteLine($"是否启动优化模式:{_BUGOptions.EnablePref}");
if (_BUGOptions.EnablePref)
{
var loadSuccess = Confluent.Kafka.Library.Load(null);
Console.WriteLine($"加载{loadSuccess},版本号:{Confluent.Kafka.Library.VersionString}");
}
}
builder
.AddConfigurationContainer()
.AddOptionsFromConfiguration()

View File

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<Import Project="../../common.props" />
<Import Project="../../common.props"/>
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
@ -10,17 +10,18 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="FluentValidation.AspNetCore" Version="11.2.2" />
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="7.0.2" />
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.17.0" />
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0" />
<PackageReference Include="Volo.Abp.Autofac" Version="7.0.2" />
<PackageReference Include="FluentValidation.AspNetCore" Version="11.2.2"/>
<PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="7.0.2"/>
<PackageReference Include="Microsoft.VisualStudio.Azure.Containers.Tools.Targets" Version="1.17.0"/>
<PackageReference Include="Swashbuckle.AspNetCore" Version="6.5.0"/>
<PackageReference Include="Volo.Abp.Autofac" Version="7.0.2"/>
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\SJZY.Track.Application\SJZY.Track.Application.csproj" />
<ProjectReference Include="..\SJZY.Track.Repository\SJZY.Track.Repository.csproj" />
<ProjectReference Include="..\SJZY.Track.Application\SJZY.Track.Application.csproj"/>
<ProjectReference Include="..\SJZY.Track.Repository\SJZY.Track.Repository.csproj"/>
</ItemGroup>
<ItemGroup>
<None Update="kafkalib\**\*" CopyToPublishDirectory="Always" CopyToOutputDirectory="Always" />
</ItemGroup>
</Project>

Binary file not shown.

Binary file not shown.

View File

@ -135,6 +135,11 @@ public class TrackChangeEto
/// </summary>
public DateTime? EstimatedDeliveryDate { get; set; }
/// <summary>
/// 目的地时区
/// </summary>
public float? DestinationTimeZone { get; set; }
/// <summary>
/// 扩展信息字段,用与轨迹推送的附加字段
/// </summary>

View File

@ -5,7 +5,7 @@
<PropertyGroup>
<TargetFramework>net7.0</TargetFramework>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
<Version>7.0.20</Version>
<Version>7.0.22</Version>
<GenerateDocumentationFile>True</GenerateDocumentationFile>
</PropertyGroup>
<ItemGroup>

View File

@ -6,7 +6,6 @@ using SJZY.Track.Application.Contract.Dtos.ProvideServices;
using SJZY.Track.Application.Contract.Events;
using SJZY.Track.Application.Contract.Services;
using SJZY.Track.Application.Kafka;
using SJZY.Track.Application.Services.Warning.Handlers;
using SJZY.Track.Application.Services.Warning.Signals;
using SJZY.Track.Domain.Aggregates.OrderAggregate;
using SJZY.Track.Domain.Aggregates.TrackDetailsAggregate;
@ -25,8 +24,7 @@ public class TrackDeleteEventHandler : ITransientDependency, ICapSubscribe
private readonly IProvideQueries _provideQueries;
private readonly ITrackDetailsEsRepository _trackDetailsEsRepository;
private readonly IBroadcastingService _broadcastingService;
// private readonly WarningSignalPublisher _warningSignalPublisher;
private readonly IServiceProvider _serviceProvider;
private readonly WarningSignalPublisher _warningSignalPublisher;
public TrackDeleteEventHandler(
IProvideQueries provideQueries,
@ -35,8 +33,7 @@ public class TrackDeleteEventHandler : ITransientDependency, ICapSubscribe
ITrackDetailsEsRepository trackDetailsEsRepository,
IOrderOperationRecordEsRepository orderOperationRecordEsRepository,
IBroadcastingService broadcastingService,
IServiceProvider serviceProvider
//WarningSignalPublisher warningSignalPublisher
WarningSignalPublisher warningSignalPublisher
)
{
_provideQueries = provideQueries;
@ -45,8 +42,7 @@ public class TrackDeleteEventHandler : ITransientDependency, ICapSubscribe
_trackDetailsEsRepository = trackDetailsEsRepository;
_orderOperationRecordEsRepository = orderOperationRecordEsRepository;
_broadcastingService = broadcastingService;
_serviceProvider = serviceProvider;
//_warningSignalPublisher = warningSignalPublisher;
_warningSignalPublisher = warningSignalPublisher;
}
[CapSubscribe(GlobalConst.TrackDeleteSubscribe, Group = GlobalConst.TrackDeleteSubscribe)]
@ -327,6 +323,7 @@ public class TrackDeleteEventHandler : ITransientDependency, ICapSubscribe
_broadcastingService.TrackChangePublicTopic(new TrackChangeEto
{
EstimatedDeliveryDate = track.EstimatedDeliveryDate,
DestinationTimeZone = track.DestinationTimeZone,
TrackPlacePostcode = track.TrackPlacePostcode,
TrackPlaceNation = track.TrackPlaceNation,
TrackPlaceNationId = track.TrackPlaceNationId,
@ -354,7 +351,7 @@ public class TrackDeleteEventHandler : ITransientDependency, ICapSubscribe
OrderNo = track.OrderNo,
OrderIdentifiers = new[]
{
new OrderIdentifier()
new OrderIdentifier
{
OrderId = track.OrderId,
OrderNo = track.OrderNo,
@ -368,43 +365,8 @@ public class TrackDeleteEventHandler : ITransientDependency, ICapSubscribe
}
}
private void WarningCalc_DeletedTrack(List<TrackDetailsIndex> delTracks)
{
if (_BUGOptions.EnablePref)
{
var scope = _serviceProvider.CreateScope();
var _warningSignalPublisher = scope.ServiceProvider.GetRequiredService<WarningSignalPublisher>();
_warningSignalPublisher.PublishWarningSignal(WarningSignalType.轨迹删除, delTracks);
scope.Dispose();
return;
}
//_warningSignalPublisher.PublishWarningSignal(WarningSignalType.轨迹删除, delTracks);
var signals = WarningSignal.FromTrack(delTracks, WarningSignalType.轨迹删除);
_ = Task.Run(async () =>
{
var scope = _serviceProvider.CreateAsyncScope();
try
{
var handler = scope.ServiceProvider.GetRequiredService<ICompleteWarningHandler>();
var handler2 = scope.ServiceProvider.GetRequiredService<ITransportWarningHandler>();
var handler3 = scope.ServiceProvider.GetRequiredService<IReturnWarningHandler>();
await Task.Delay(1500); //确保落盘
await handler.TrackDeletedHandleAsync(signals);
await handler2.TrackDeletedHandleAsync(signals);
await handler3.TrackUpdatedOrDeletedHandleAsync(signals);
}
catch (Exception ex)
{
LogHelper.Error(ex);
}
finally
{
await scope.DisposeAsync();
}
});
_warningSignalPublisher.PublishSignal(WarningSignalType.轨迹删除, delTracks);
}
}

View File

@ -463,7 +463,7 @@ public class TrackPushEventHandler : ITransientDependency, ICapSubscribe
if (string.IsNullOrWhiteSpace(compareFields))
return trackIdentifies.Contains($"{code}{timeStr}");
var extra = x.GetValueOrDefault("extra");
var extra = x.GetValueOrDefault("extra");
if (extra == null) return false;
if (((JsonElement) extra).TryGetProperty(compareFields, out var comparerValueJsonElement))
@ -538,15 +538,22 @@ public class TrackPushEventHandler : ITransientDependency, ICapSubscribe
if (config.DateTimeFormatType == (int) DateTimeFormatEnum.Timestamp)
return DateTimeHelper.ConvertToUnixTimestamp(dateTime).ToString();
if (config.DateTimeFormatType == (int) DateTimeFormatEnum.DateTimeAndTimezone
&& !string.IsNullOrEmpty(config.DateTimeFormat)
&& !string.IsNullOrEmpty(config.TimezoneFormat))
if (config.DateTimeFormatType == (int) DateTimeFormatEnum.DateTimeAndTimezone)
{
var zoneStr = DateTimeHelper.TimezoneFormat(timezone, config.TimezoneFormat);
var datetimeStr = DateTimeHelper.FormatDateTime(dateTime, config.DateTimeFormat);
return $"{datetimeStr} {zoneStr}";
// 只有时间
if (!string.IsNullOrWhiteSpace(config.DateTimeFormat) && string.IsNullOrWhiteSpace(config.TimezoneFormat))
return DateTimeHelper.FormatDateTime(dateTime, config.DateTimeFormat);
// 时间和时区
if (!string.IsNullOrWhiteSpace(config.DateTimeFormat) && !string.IsNullOrWhiteSpace(config.TimezoneFormat))
{
var zoneStr = DateTimeHelper.TimezoneFormat(timezone, config.TimezoneFormat);
var datetimeStr = DateTimeHelper.FormatDateTime(dateTime, config.DateTimeFormat);
return $"{datetimeStr} {zoneStr}";
}
}
// 默认为 时间+时区
return dateTime.ToString("yyyy-MM-ddTHH:mm:ss.fffK");
}

View File

@ -6,7 +6,6 @@ using SJZY.OTel;
using SJZY.Track.Application.Contract.Events;
using SJZY.Track.Application.Contract.Services;
using SJZY.Track.Application.Kafka;
using SJZY.Track.Application.Services.Warning.Handlers;
using SJZY.Track.Application.Services.Warning.Signals;
using SJZY.Track.Domain.Aggregates.OrderAggregate;
using SJZY.Track.Domain.Aggregates.TrackDetailsAggregate;
@ -26,11 +25,8 @@ public class TrackUpsertEventHandler : ITransientDependency, ICapSubscribe
private readonly ITrackDetailsEsRepository _trackDetailsEsRepository;
private readonly IBroadcastingService _broadcastingService;
private readonly ILogger<TrackUpsertEventHandler> _logger;
private readonly IOrderOperationRecordEsRepository _orderOperationRecordEsRepository;
//private readonly WarningSignalPublisher _warningSignalPublisher;
private readonly IServiceProvider _serviceProvider;
private readonly WarningSignalPublisher _warningSignalPublisher;
public TrackUpsertEventHandler(
IOrderRepository orderRepository,
@ -40,8 +36,7 @@ public class TrackUpsertEventHandler : ITransientDependency, ICapSubscribe
IOrderOperationRecordEsRepository orderOperationRecordEsRepository,
IBroadcastingService broadcastingService,
ILogger<TrackUpsertEventHandler> logger,
IServiceProvider serviceProvider
//WarningSignalPublisher warningSignalPublisher
WarningSignalPublisher warningSignalPublisher
)
{
_orderRepository = orderRepository;
@ -51,8 +46,7 @@ public class TrackUpsertEventHandler : ITransientDependency, ICapSubscribe
_orderOperationRecordEsRepository = orderOperationRecordEsRepository;
_broadcastingService = broadcastingService;
_logger = logger;
_serviceProvider = serviceProvider;
//_warningSignalPublisher = warningSignalPublisher;
_warningSignalPublisher = warningSignalPublisher;
}
[CapSubscribe(GlobalConst.TrackUpsertSubscribe, Group = GlobalConst.TrackUpsertSubscribe)]
@ -102,11 +96,10 @@ public class TrackUpsertEventHandler : ITransientDependency, ICapSubscribe
// 不同子系统需要做内容映射
if (@event.SubSystemId != nextEvent.SubSystemId)
{
var mapCode = (await _provideQueries
.MapTrackCodesAsync(@event.SubSystemId, nextEvent.SubSystemId, @event.TrackCode))
var mapCode = (await _provideQueries.MapTrackCodesAsync(@event.SubSystemId, nextEvent.SubSystemId, @event.TrackCode))
.FirstOrDefault();
if (mapCode == default)
if (mapCode == null)
{
// 跨子系统没做映射的轨迹不处理
continue;
@ -518,75 +511,12 @@ public class TrackUpsertEventHandler : ITransientDependency, ICapSubscribe
private void WarningCalc_AddedTrack(List<TrackDetailsIndex> tracks)
{
if (_BUGOptions.EnablePref)
{
var scope = _serviceProvider.CreateScope();
var _warningSignalPublisher = scope.ServiceProvider.GetRequiredService<WarningSignalPublisher>();
_warningSignalPublisher.PublishWarningSignal(WarningSignalType.轨迹新增, tracks);
scope.Dispose();
return;
}
//_warningSignalPublisher.PublishWarningSignal(WarningSignalType.轨迹新增, tracks);
var signals = WarningSignal.FromTrack(tracks, WarningSignalType.轨迹新增);
_ = Task.Run(async () =>
{
var scope = _serviceProvider.CreateAsyncScope();
try
{
var handler = scope.ServiceProvider.GetRequiredService<ICompleteWarningHandler>();
var handler2 = scope.ServiceProvider.GetRequiredService<ITransportWarningHandler>();
await Task.Delay(1500); //确保落盘
await handler.TrackAddedHandleAsync(signals);
await handler2.TrackAddedOrUpdatedHandleAsync(signals);
}
catch (Exception ex)
{
LogHelper.Error(ex);
}
finally
{
await scope.DisposeAsync();
}
});
_warningSignalPublisher.PublishSignal(WarningSignalType.轨迹新增, tracks);
}
private void WarningCalc_UpdatedTrack(List<TrackDetailsIndex> tracks)
{
if (_BUGOptions.EnablePref)
{
var scope = _serviceProvider.CreateScope();
var _warningSignalPublisher = scope.ServiceProvider.GetRequiredService<WarningSignalPublisher>();
_warningSignalPublisher.PublishWarningSignal(WarningSignalType.轨迹更新, tracks);
scope.Dispose();
return;
}
//_warningSignalPublisher.PublishWarningSignal(WarningSignalType.轨迹更新, tracks);
var signals = WarningSignal.FromTrack(tracks, WarningSignalType.轨迹更新);
_ = Task.Run(async () =>
{
var scope = _serviceProvider.CreateAsyncScope();
try
{
var handler = scope.ServiceProvider.GetRequiredService<ITransportWarningHandler>();
var handler2 = scope.ServiceProvider.GetRequiredService<IReturnWarningHandler>();
await Task.Delay(1500); //确保落盘
await handler.TrackAddedOrUpdatedHandleAsync(signals);
await handler2.TrackUpdatedOrDeletedHandleAsync(signals);
}
catch (Exception ex)
{
LogHelper.Error(ex);
}
finally
{
await scope.DisposeAsync();
}
});
_warningSignalPublisher.PublishSignal(WarningSignalType.轨迹更新, tracks);
}
}

View File

@ -1,63 +0,0 @@
namespace SJZY.Track.Application.Hangfire;
// 发送消息接口参数
// https://centerapi.test.shijizhongyun.com/userApi/index.html?urls.primaryName=%E6%B6%88%E6%81%AF%E8%AE%A2%E9%98%85%E5%AE%9E%E4%BE%8B
/// MsgSubscribeInstance/SendMessageByTemplate
public class NotifyConfig
{
private const string ContentKey = "content"; //内容参数
public bool IsNeedToHandle { get; set; } = true; //是否需要处理(默认为未处理,传false为无需处理,针对站内信是否处理功能)
public int MsgSource { get; set; } = 1; // 消息来源:1.消息 2.任务 3.流程 4.消息提醒
public int SystemId { get; set; } = 9498; //子系统id 业务中台
public string TemplateCode { get; set; } = "TrackWarningNotify"; //模板code
public List<int> Channels { get; set; } //1:站内信 2.飞书 4.邮箱 8.短信 16.语音播报
public List<Variable> Variables { get; set; } //模板参数
public List<Receiver> Receivers { get; set; }
public sealed record Variable(string Key, string Value);
public sealed record Receiver(
int ReceiverId, //接收人id : 内部用户Id,外部用户Id,或只有邮箱跟短信时不需要接收人Id
int RecipientType = 1, //接收人类型 (1:内部用户 2.外部用户)
string? ReceiverName = null, //接收人姓名
string? Mail = null, //邮箱
string? MobilePhone = null //手机号
);
public static NotifyConfig CreateConfig(int userId, string message, List<int> notifyModes)
{
return new NotifyConfig
{
Channels = notifyModes,
Receivers = new List<Receiver>(1)
{
new Receiver(userId)
},
Variables = new List<Variable>(1)
{
new Variable(ContentKey, message)
}
};
}
public static NotifyConfig CreateConfig(int userId, string message)
{
return new NotifyConfig
{
Channels = new List<int>(2)
{
1,
2
},
Receivers = new List<Receiver>
{
new Receiver(userId)
},
Variables = new List<Variable>
{
new Variable(ContentKey, message)
}
};
}
}

View File

@ -1,6 +1,5 @@
using Microsoft.Extensions.Options;
using SJZY.Track.Application.Contract.Dtos.Warning;
using SJZY.Track.Application.Helpers;
using SJZY.Track.Application.Options;
using SJZY.Track.Application.Services.Warning.Model;
using SJZY.Track.Domain.Aggregates.KPIAreaAggregate;
@ -43,7 +42,7 @@ public sealed class WarningNotifyAggregator : ITransientDependency
// 2.以人的维度聚合通知信息,并生成配置参数NotifyConfig
// 3.构建并调度一个调用通知接口的立即执行任务
// 4.绑定这个任务id(可能是多个)和jobId用户跟踪任务状态
public async Task<(List<NotifyConfig> NotifyConfigs, HashSet<int> taskIds)> ScanNotifyAndAggregateAsync()
public async Task<(List<WarningNotifyParam> NotifyConfigs, HashSet<int> taskIds)> ScanNotifyAndAggregateAsync()
{
// 构建查询游标 todo:全量查询需要优化,直接存缓存?
// var options = _optionsMonitor.CurrentValue;
@ -210,7 +209,7 @@ public sealed class WarningNotifyAggregator : ITransientDependency
// 2.检查明细中为人工介入的状态
var detailsIdSet = filtered.Select(x => x.WarningDetailsId).ToHashSet();
var warningDetails = await _warningDetailsEsRepository.GetReturnWarningDetailsAsync(new GetReturnWarningDetailsVO()
var warningDetails = await _warningDetailsEsRepository.GetReturnWarningDetailsAsync(new GetReturnWarningDetailsVO
{
DetailsIds = detailsIdSet,
Status = new HashSet<int>(1)
@ -261,10 +260,9 @@ public sealed class WarningNotifyAggregator : ITransientDependency
return result;
}
private List<NotifyConfig> BuildNotifyConfigs(List<WarningNotifyTaskEntity> needNotifyTasks)
private List<WarningNotifyParam> BuildNotifyConfigs(List<WarningNotifyTaskEntity> needNotifyTasks)
{
var systemId = _optionsMonitor.CurrentValue.MiddleSystemId;
var result = new List<NotifyConfig>();
var result = new List<WarningNotifyParam>();
if (needNotifyTasks.Count == 0) return result;
// 1.以人的维度对不同类别的预警通知分组聚合
@ -293,109 +291,51 @@ public sealed class WarningNotifyAggregator : ITransientDependency
}
}
// 2.使用消息模板构建通知消息,并发送通知
// 2.使用消息模板构建通知参数,稍后排队发送通知
foreach (var (userId, typeNotifyTaskDict) in userIdNotifyTaskDict)
{
var notifyMessage = BuildNotifyMessage(typeNotifyTaskDict);
//聚合时出现 用户a
//接收回传预警,命中规则1,通知方式(飞书,站内信) ,命中规则2,通知方式(飞书)
//接收运输预警,命中规则1,通知方式(飞书,站内信) ,命中规则2,通知方式(飞书)
//接收完整预警,命中规则1,通知方式(飞书) ,命中规则2,通知方式(飞书,站内信)
//此时聚合后,消息为:总共6条预警消息,其中回传xx条,运输xx条,完整xx条
//同时使用飞书和站内信两种方式通知
var config = NotifyConfig.CreateConfig(userId, notifyMessage, new List<int>
{
1,
2
});
config.SystemId = systemId;
result.Add(config);
result.Add(BuildNotifyParam(userId, typeNotifyTaskDict));
}
return result;
}
private string BuildNotifyMessage(Dictionary<int, List<WarningNotifyTaskEntity>> typeNotifyTaskDict)
private WarningNotifyParam BuildNotifyParam(int userId, Dictionary<int, List<WarningNotifyTaskEntity>> typeNotifyTaskDict)
{
var options = _optionsMonitor.CurrentValue;
var hours = options.ScanWarningNotifyTaskHourInterval;
var transportCount = 0;
var transportLastNHoursCount = 0;
var returnCount = 0;
var returnLastNHoursCount = 0;
var completeCount = 0;
var completeLastNHoursCount = 0;
Func<WarningNotifyTaskEntity, bool> hoursNBefore = x => DateTime.Now - x.CreateTime <= TimeSpan.FromHours(hours);
var param = new WarningNotifyParam
{
UserId = userId,
Hours = hours
};
// tips:获取过去x小时的增量
Func<WarningNotifyTaskEntity, bool> hoursNBefore = x => DateTime.Now - x.CreateTime <= TimeSpan.FromHours(hours);
if (typeNotifyTaskDict.TryGetValue(WarningRuleType.运输时效预警.GetHashCode(), out var transportTimelinessNotifyTasks))
{
transportLastNHoursCount = transportTimelinessNotifyTasks.Count(hoursNBefore);
transportCount = transportTimelinessNotifyTasks.Count;
param.TransportLastNHoursCount = transportTimelinessNotifyTasks.Count(hoursNBefore);
param.TransportCount = transportTimelinessNotifyTasks.Count;
}
if (typeNotifyTaskDict.TryGetValue(WarningRuleType.回传时效预警.GetHashCode(), out var returnTimelinessNotifyTasks))
{
returnLastNHoursCount = returnTimelinessNotifyTasks.Count(hoursNBefore);
returnCount = returnTimelinessNotifyTasks.Count;
param.ReturnLastNHoursCount = returnTimelinessNotifyTasks.Count(hoursNBefore);
param.ReturnCount = returnTimelinessNotifyTasks.Count;
}
if (typeNotifyTaskDict.TryGetValue(WarningRuleType.完整预警.GetHashCode(), out var completeWarningNotifyTasks))
{
completeLastNHoursCount = completeWarningNotifyTasks.Count(hoursNBefore);
completeCount = completeWarningNotifyTasks.Count;
param.CompleteLastNHoursCount = completeWarningNotifyTasks.Count(hoursNBefore);
param.CompleteCount = completeWarningNotifyTasks.Count;
}
return NotifyMessageBuilder.Build(
hours,
transportCount,
transportLastNHoursCount,
returnCount,
returnLastNHoursCount,
completeCount,
completeLastNHoursCount
);
}
}
// 标题:预警通知
// 内容:尊敬的用户,截止到2024-12-13 10:00:01(UTC +8:00),轨迹中心监测到与您相关的预警风险信息共计299 条,请及时处理,详情如下:
// 运输时效预警共计100条,其中过去4小时内新增的预警10条,查看详情
// 回传时效预警共计99条,其中过去4小时内新增的预警10条,查看详情
// 轨迹完整预警共计100条,其中过去4小时内新增的预警10条,查看详情
public static class NotifyMessageBuilder
{
public const string Title = "预警通知";
public static string Build(int hours,
int transportCount, int transportLastNHoursCount,
int returnCount, int returnLastNHoursCount,
int completeCount, int completeLastNHoursCount)
{
var dateTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
var totalCount = transportCount + returnCount + completeCount;
// todo:无法使用a标签填充(可能是为了安全考虑),需要具体给定一个样式
var url1 = "";
var url2 = "";
var url3 = "";
var message = $"{Title}\n尊敬的用户,截止到{dateTime}({BuildLocalTimezoneStr()}),轨迹中心监测到与您相关的预警风险信息共计{totalCount} 条,请及时处理,详情如下:";
if (transportCount > 0)
message += $"\n运输时效预警共计{transportCount}条,其中过去{hours}小时内新增的预警{transportLastNHoursCount}条";
if (returnCount > 0)
message += $"\n回传时效预警共计{returnCount}条,其中过去{hours}小时内新增的预警{returnLastNHoursCount}条";
if (completeCount > 0)
message += $"\n轨迹完整预警共计{completeCount}条,其中过去{hours}小时内新增的预警{completeLastNHoursCount}条";
return message;
}
// 构建本地时区字符串标识 e.g UTC +8:00
private static string BuildLocalTimezoneStr()
{
var offset = TimeZoneInfo.Local.BaseUtcOffset;
var flag = offset.Hours > 0 ? "+" : "";
var offsetStr = offset.ToString().RemovePostFix(":00");
return $"UTC {flag}{offsetStr}";
return param;
}
}

View File

@ -0,0 +1,122 @@
using FeishuNetSdk;
using FeishuNetSdk.Im;
using FeishuNetSdk.Im.Dtos;
using SJZY.UserCenter.SharedModel.Dtos.Message;
using SJZY.UserCenter.SharedModel.Enums;
namespace SJZY.Track.Application.Hangfire;
// 标题:预警通知
// 内容:尊敬的用户,截止到2024-12-13 10:00:01(UTC +8:00),轨迹中心监测到与您相关的预警风险信息共计299 条,请及时处理,详情如下:
// 运输时效预警共计100条,其中过去4小时内新增的预警10条,查看详情
// 回传时效预警共计99条,其中过去4小时内新增的预警10条,查看详情
// 轨迹完整预警共计100条,其中过去4小时内新增的预警10条,查看详情
public static class WarningNotifyBuilder
{
private const string Title = "预警通知";
// 构建站内信发消息请求体
public static SendMessageByTemplateInput BuildZhanNeiXinMessage(WarningNotifyParam param, int systemId)
{
const string templateCode = "TrackWarningNotify"; // 模板code
const string contentKey = "content"; //内容参数
var dateTime = DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss");
var totalCount = param.TransportCount + param.ReturnCount + param.CompleteCount;
// todo:无法使用a标签填充(可能是为了安全考虑),需要具体给定一个样式
var url1 = "";
var url2 = "";
var url3 = "";
var content = $"{Title}\n尊敬的用户,截止到{dateTime}({BuildLocalTimezoneStr()}),轨迹中心监测到与您相关的预警风险信息共计{totalCount} 条,请及时处理,详情如下:";
if (param.TransportCount > 0)
content += $"\n运输时效预警共计{param.TransportCount}条,其中过去{param.Hours}小时内新增的预警{param.TransportLastNHoursCount}条";
if (param.ReturnCount > 0)
content += $"\n回传时效预警共计{param.ReturnCount}条,其中过去{param.Hours}小时内新增的预警{param.ReturnLastNHoursCount}条";
if (param.CompleteCount > 0)
content += $"\n轨迹完整预警共计{param.CompleteCount}条,其中过去{param.Hours}小时内新增的预警{param.CompleteLastNHoursCount}条";
// 构建用户中心发消息请求体对象
var dto = new SendMessageByTemplateInput
{
IsNeedToHandle = true,
MsgSource = MsgSourceEnum.消息,
SystemId = systemId,
templateCode = templateCode,
channels = new List<ChannelEnum>(1)
{
ChannelEnum.站内信
},
variables = new List<VariableDto>(1)
{
new VariableDto
{
Key = contentKey,
Value = content
}
},
receivers = new List<MsgInstanceReceiverInput>(1)
{
new MsgInstanceReceiverInput
{
RecipientType = 1, //接收人类型 (1:内部用户 2.外部用户)
ReceiverId = param.UserId, //接收人id
ReceiverName = null, //接收人名称
Mail = null, // 邮箱
MobilePhone = null, // 手机号
}
},
};
return dto;
}
// 构建飞书卡片发消息请求体
public static PostImV1MessagesBodyDto BuildFeiShuCard(WarningNotifyParam param, string fsUserId)
{
var totalCount = param.TransportCount + param.ReturnCount + param.CompleteCount;
var body = new List<object>
{
new MarkdownElement(Content: $"尊敬的用户,截止到{DateTime.Now:yyyy-MM-dd HH:mm:ss}({BuildLocalTimezoneStr()}),轨迹中心监测到与您相关的预警风险信息共计{totalCount} 条,请及时处理,详情如下:", TextAlign: "left", TextSize: "normal_v2")
};
// todo:补充点击链接
var url1 = "";
var url2 = "";
var url3 = "";
if (param.TransportCount > 0)
body.Add(new MarkdownElement(Content: $"运输时效预警共计{param.TransportCount}条,其中过去{param.Hours}小时内新增的预警{param.TransportLastNHoursCount}条 [查看详情]({url1})", TextAlign: "left", TextSize: "normal_v2"));
if (param.ReturnCount > 0)
body.Add(new MarkdownElement(Content: $"轨迹回传时效预警共计{param.ReturnCount}条,其中过去{param.Hours}小时内新增的预警{param.ReturnLastNHoursCount}条 [查看详情]({url2})", TextAlign: "left", TextSize: "normal_v2"));
if (param.CompleteCount > 0)
body.Add(new MarkdownElement(Content: $"轨迹完整预警共计{param.CompleteCount}条,其中过去{param.Hours}小时内新增的预警{param.CompleteLastNHoursCount}条 [查看详情]({url3})", TextAlign: "left", TextSize: "normal_v2"));
var dto = new PostImV1MessagesBodyDto
{
MsgType = "interactive",
ReceiveId = fsUserId
};
dto.SetContent(new ElementsCardV2Dto
{
// 配置卡片全局行为
Config = new ElementsCardV2Dto.ConfigSuffix(),
Header = new ElementsCardV2Dto.HeaderSuffix()
{
Title = new HeaderTitleElement(Title),
Template = "red"
},
Body = new ElementsCardV2Dto.BodySuffix(body.ToArray()),
});
return dto;
}
// 构建本地时区字符串标识 e.g UTC +8:00
private static string BuildLocalTimezoneStr()
{
var offset = TimeZoneInfo.Local.BaseUtcOffset;
var flag = offset.Hours > 0 ? "+" : "";
var offsetStr = offset.ToString().RemovePostFix(":00");
return $"UTC {flag}{offsetStr}";
}
}

View File

@ -0,0 +1,23 @@
namespace SJZY.Track.Application.Hangfire;
/// <summary>
/// 构建轨迹预警通知消息所需的参数
/// </summary>
public class WarningNotifyParam
{
public int UserId { get; set; }
public int Hours { get; set; }
public int TransportCount { get; set; }
public int TransportLastNHoursCount { get; set; }
public int ReturnCount { get; set; }
public int ReturnLastNHoursCount { get; set; }
public int CompleteCount { get; set; }
public int CompleteLastNHoursCount { get; set; }
}

View File

@ -1,3 +1,4 @@
using FeishuNetSdk;
using Hangfire;
using Microsoft.Extensions.Options;
using SJZY.Expand.ABP.Core.Logger;
@ -11,9 +12,8 @@ using SJZY.Track.Domain.Aggregates.WarningAggregate.Return;
using SJZY.Track.Domain.Dtos.Warning;
using SJZY.Track.Domain.Enums;
using SJZY.Track.Domain.Extensions;
using SJZY.Track.Domain.Queries;
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;
@ -21,8 +21,8 @@ namespace SJZY.Track.Application.Hangfire;
public interface IWarningTaskService
{
[AutomaticRetry(Attempts = 3)]
Task CallNotifyAsync(NotifyConfig config, CancellationToken ct);
[AutomaticRetry(Attempts = 1)]
Task CallNotifyAsync(WarningNotifyParam param, CancellationToken ct);
[AutomaticRetry(Attempts = 3, ExceptOn = new[] { typeof(BusinessException) })]
Task CallTrackRetryPushAsync(ReturnWarningRetryPushTaskEntity entity, CancellationToken ct);
@ -30,51 +30,57 @@ public interface IWarningTaskService
public class WarningTaskService : IWarningTaskService, ITransientDependency
{
private readonly IOptionsMonitor<InterfaceRelayParamsConfigOptions> _interfaceRelayOptions;
private readonly IOptionsMonitor<TrackWarningTaskOptions> _warningTaskOptions;
private readonly IHttpClientProxy<IRequestSendMessageService> _requestSendMessageService;
private readonly IHttpClientProxy<IInterfaceForwardServiceV2> _interfaceForwardServiceV2;
private readonly IOptionsMonitor<InterfaceRelayParamsConfigOptions> _options;
private readonly IFeishuTenantApi _feishuTenantApi;
private readonly IWarningDetailsEsRepository _warningDetailsEsRepository;
private readonly IWarningTaskRepository _warningTaskRepository;
private readonly IVFsUserQueries _vfsUserQueries;
public WarningTaskService(IHttpClientProxy<IInterfaceForwardServiceV2> interfaceForwardServiceV2, IOptionsMonitor<InterfaceRelayParamsConfigOptions> options, IHttpClientProxy<IRequestSendMessageService> requestSendMessageService, IWarningDetailsEsRepository warningDetailsEsRepository, IWarningTaskRepository warningTaskRepository)
public WarningTaskService(
IOptionsMonitor<InterfaceRelayParamsConfigOptions> interfaceRelayOptions,
IOptionsMonitor<TrackWarningTaskOptions> warningTaskOptions,
IHttpClientProxy<IInterfaceForwardServiceV2> interfaceForwardServiceV2,
IHttpClientProxy<IRequestSendMessageService> requestSendMessageService,
IFeishuTenantApi feishuTenantApi,
IWarningDetailsEsRepository warningDetailsEsRepository,
IWarningTaskRepository warningTaskRepository,
IVFsUserQueries vfsUserQueries)
{
_interfaceRelayOptions = interfaceRelayOptions;
_warningTaskOptions = warningTaskOptions;
_interfaceForwardServiceV2 = interfaceForwardServiceV2;
_requestSendMessageService = requestSendMessageService;
_feishuTenantApi = feishuTenantApi;
_warningDetailsEsRepository = warningDetailsEsRepository;
_warningTaskRepository = warningTaskRepository;
_options = options;
_vfsUserQueries = vfsUserQueries;
}
#region 用户中心调用,通知(一次)
[AutomaticRetry(Attempts = 3)]
public async Task CallNotifyAsync(NotifyConfig config, CancellationToken ct)
[AutomaticRetry(Attempts = 1)]
public async Task CallNotifyAsync(WarningNotifyParam param, CancellationToken ct)
{
ct.ThrowIfCancellationRequested();
var r = await _requestSendMessageService.Service.SendMessageByTemplateAsync(new SendMessageByTemplateInput
{
IsNeedToHandle = config.IsNeedToHandle,
MsgSource = (MsgSourceEnum) config.MsgSource,
SystemId = config.SystemId,
templateCode = config.TemplateCode,
channels = config.Channels.Select(x => (ChannelEnum) x).ToList(),
variables = config.Variables.Select(x => new VariableDto()
{
Key = x.Key,
Value = x.Value
}).ToList(),
receivers = config.Receivers.Select(x => new MsgInstanceReceiverInput()
{
ReceiverId = x.ReceiverId,
RecipientType = x.RecipientType
}).ToList(),
});
var options = _warningTaskOptions.CurrentValue;
// 发送站内信
// https://centerapi.test.shijizhongyun.com/userApi/index.html?urls.primaryName=%E6%B6%88%E6%81%AF%E8%AE%A2%E9%98%85%E5%AE%9E%E4%BE%8B
// MsgSubscribeInstance/SendMessageByTemplate
var r = await _requestSendMessageService.Service.SendMessageByTemplateAsync(WarningNotifyBuilder.BuildZhanNeiXinMessage(param, options.MiddleSystemId));
if (r.Code != GlobalConst.SuccessCode)
{
throw new BusinessException(message: $"用户中心调用失败:SendMessageByTemplateAsync" +
$"Msg:{r.Msg}");
}
throw new BusinessException(message: $"用户中心调用失败:SendMessageByTemplateAsync Msg:{r.Msg}");
// 发送FeiShu卡片信息
var fsUserId = await _vfsUserQueries.GetFirstAsync(x => x.UserCenterId == param.UserId, y => y.FsEmployeeId);
if (string.IsNullOrWhiteSpace(fsUserId))
throw new BusinessException(message: "查询用户中心失败:SendMessageByTemplateAsync Msg:未找到用户对应的飞书用户Id");
var res = await _feishuTenantApi.PostImV1MessagesAsync("user_id", WarningNotifyBuilder.BuildFeiShuCard(param, fsUserId));
if (res.Code != 0)
throw new BusinessException(message: $"飞书调用失败:SendMessageByTemplateAsync Msg:{res.Msg}");
}
#endregion
@ -107,8 +113,8 @@ public class WarningTaskService : IWarningTaskService, ITransientDependency
using (Track2InterfaceRelaySubSystemContext.Use(new Track2InterfaceRelaySubSystemContext(task.SubSystemCode)))
{
var upStreamCode = isAdd
? _options.CurrentValue.PushTrackInterfaceCode
: _options.CurrentValue.TrackUpdateInterfaceCode;
? _interfaceRelayOptions.CurrentValue.PushTrackInterfaceCode
: _interfaceRelayOptions.CurrentValue.TrackUpdateInterfaceCode;
var r = await _interfaceForwardServiceV2.Service.BatchForwardThenPushToQueueAsync(
upStreamCode: upStreamCode,

View File

@ -1,7 +1,5 @@
using System.Diagnostics;
using Confluent.Kafka;
using Confluent.Kafka.Core.Consumer;
using Confluent.Kafka.Core.Producer;
using Microsoft.Extensions.Hosting;
using Microsoft.Extensions.Options;
using SJZY.Expand.ABP.Core.Logger;
@ -20,23 +18,20 @@ namespace SJZY.Track.Application.Kafka;
public sealed class WarningCalcBackgroundService : BackgroundService
{
private readonly IServiceScopeFactory _serviceScopeFactory;
private readonly IKafkaConsumer<WarningSignalKey, WarningSignal> _consumer;
private readonly IKafkaProducer<WarningSignalKey, WarningSignal> _producer;
private readonly IKafkaProducer<int, WarningSignal> _dlxProducer;
private readonly IConsumer<WarningSignalKey, WarningSignal> _consumer;
private readonly WarningSignalPublisher _warningSignalPublisher;
private TrackWarningTaskOptions _options;
public WarningCalcBackgroundService(
IOptionsMonitor<TrackWarningTaskOptions> optionsMonitor,
IServiceScopeFactory serviceScopeFactory,
IKafkaConsumer<WarningSignalKey, WarningSignal> consumer,
IKafkaProducer<WarningSignalKey, WarningSignal> producer,
IKafkaProducer<int, WarningSignal> dlxProducer
IConsumer<WarningSignalKey, WarningSignal> consumer,
WarningSignalPublisher warningSignalPublisher
)
{
_serviceScopeFactory = serviceScopeFactory;
_consumer = consumer;
_producer = producer;
_dlxProducer = dlxProducer;
_warningSignalPublisher = warningSignalPublisher;
_options = optionsMonitor.CurrentValue;
optionsMonitor.OnChange(x => _options = x);
}
@ -175,7 +170,7 @@ public sealed class WarningCalcBackgroundService : BackgroundService
var tasks = needRetryMetadata.Select(async metadata =>
{
metadata.SetTrackSignalMessageHeader();
await _producer.ProduceAsync(metadata.OwenMessage, CancellationToken.None);
await _warningSignalPublisher.PublishSignalAsync(metadata.OwenMessage, CancellationToken.None);
}).ToArray();
await Task.WhenAll(tasks);
@ -193,7 +188,7 @@ public sealed class WarningCalcBackgroundService : BackgroundService
{
metadata.SetTrackSignalMessageHeader();
var msg = metadata.OwenMessage;
await _dlxProducer.ProduceAsync(new Message<int, WarningSignal>
await _warningSignalPublisher.PublishDlxSignalAsync(new Message<int, WarningSignal>
{
Key = msg.Value.OrderId,
Value = msg.Value,
@ -282,6 +277,5 @@ public sealed class WarningCalcBackgroundService : BackgroundService
base.Dispose();
_consumer.Close();
_consumer.Dispose();
_producer.Dispose();
}
}

View File

@ -1,20 +1,13 @@
//#define ProtoBuf
#define NewtonsoftJson
using System.Globalization;
using System.Net;
using System.Text;
using System.Text.Json;
using System.Text.Json.Serialization;
using Confluent.Kafka;
using Confluent.Kafka.Core.Consumer;
using Confluent.Kafka.Core.Producer;
using Microsoft.Extensions.Configuration;
using Microsoft.Extensions.Logging;
using Newtonsoft.Json;
using Newtonsoft.Json.Converters;
using Newtonsoft.Json.Serialization;
using Microsoft.Extensions.DependencyInjection.Extensions;
using SJZY.Expand.ABP.Core.Logger;
using SJZY.Track.Application.Helpers;
using SJZY.Track.Application.Services.Warning.Signals;
using JsonSerializer = System.Text.Json.JsonSerializer;
namespace SJZY.Track.Application.Kafka;
@ -22,6 +15,7 @@ public static class WarningCalcServiceExtension
{
public static void AddWarningCalcServices(this IServiceCollection services, IConfiguration conf)
{
PrintKafkaLibVersion();
var env = "-" + conf.GetValue<string>("Environment")!.ToLower();
// 预警信号批处理后台任务
services.AddHostedService<WarningCalcBackgroundService>();
@ -37,140 +31,194 @@ public static class WarningCalcServiceExtension
private static void RegisterConsumer<TKey, TValue>(IServiceCollection services, IConfiguration conf, string groupId, string[] subTopic)
{
services.AddSingleton<IKafkaConsumer<TKey, TValue>>(sp =>
services.AddJsonSerializer<TKey>();
services.AddJsonSerializer<TValue>();
services.AddSingleton<IConsumer<TKey, TValue>>(sp =>
{
var bb = new KafkaConsumerBuilder<TKey, TValue>()
.WithConsumerConfiguration(o =>
{
o.WithClientId(Dns.GetHostName())
.WithGroupId(groupId)
.WithBootstrapServers(conf.GetValue<string>("Kafka:BootstrapServers"))
.WithAutoOffsetReset(AutoOffsetReset.Earliest)
//偏移提交控制
.WithEnablePartitionEof(false)
.WithEnableAutoOffsetStore(false)
.WithEnableAutoCommit(false)
//链接控制
.WithSessionTimeoutMs(conf.GetValue<int>("Kafka:Consumer:SessionTimeoutMs"))
.WithMaxPollIntervalMs(conf.GetValue<int>("Kafka:Consumer:MaxPollIntervalMs"))
.WithHeartbeatIntervalMs(conf.GetValue<int>("Kafka:Consumer:HeartbeatIntervalMs"))
//吞吐量
.WithFetchMinBytes(conf.GetValue<int>("Kafka:Consumer:FetchMinBytes"))
.WithFetchMaxBytes(conf.GetValue<int>("Kafka:Consumer:FetchMaxBytes"))
.WithFetchWaitMaxMs(conf.GetValue<int>("Kafka:Consumer:FetchWaitMaxMs"))
//日志
.WithDebug("consumer,cgrp,protocol")
.WithTopicSubscriptions(subTopic);
});
bb.WithLoggerFactory(sp.GetRequiredService<ILoggerFactory>());
DebounceHelper.DebounceWrapper? reportFuncWrapper = null;
bb.WithErrorHandler((consumer, error) =>
{
if (error.IsFatal)
var bb = new ConsumerBuilder<TKey, TValue>(
new ConsumerConfig
{
ClientId = Dns.GetHostName(),
GroupId = groupId,
BootstrapServers = conf.GetValue<string>("Kafka:BootstrapServers"),
AutoOffsetReset = AutoOffsetReset.Earliest,
//偏移提交控制
EnablePartitionEof = false,
EnableAutoOffsetStore = false,
EnableAutoCommit = false,
//链接控制
SessionTimeoutMs = conf.GetValue<int>("Kafka:Consumer:SessionTimeoutMs"),
MaxPollIntervalMs = conf.GetValue<int>("Kafka:Consumer:MaxPollIntervalMs"),
HeartbeatIntervalMs = conf.GetValue<int>("Kafka:Consumer:HeartbeatIntervalMs"),
//吞吐量
FetchMinBytes = conf.GetValue<int>("Kafka:Consumer:FetchMinBytes"),
FetchMaxBytes = conf.GetValue<int>("Kafka:Consumer:FetchMaxBytes"),
FetchWaitMaxMs = conf.GetValue<int>("Kafka:Consumer:FetchWaitMaxMs"),
//日志
Debug = "consumer,cgrp,protocol",
CancellationDelayMaxMs = 100
})
.SetKeyDeserializer(sp.GetRequiredService<JsonCoreSerializer<TKey>>())
.SetValueDeserializer(sp.GetRequiredService<JsonCoreSerializer<TValue>>())
.SetErrorHandler((_, error) =>
{
LogHelper.Error($"Kafka致命错误${error}");
return;
}
if (error.IsFatal)
{
LogHelper.Error($"Kafka致命错误${error}");
return;
}
// tips: 防抖以防止高频上报错误
reportFuncWrapper ??= DebounceHelper.WrapMethod(TimeSpan.FromSeconds(5), () =>
{
if (error.Code == ErrorCode.Local_Transport)
LogHelper.Error($"Kafka链接错误{error}");
else if (error.Code == ErrorCode.Local_AllBrokersDown)
LogHelper.Error($"Kafka所有Broker都挂了{error}");
// tips: 防抖以防止高频上报错误
reportFuncWrapper ??= DebounceHelper.WrapMethod(TimeSpan.FromSeconds(5), () =>
{
if (error.Code == ErrorCode.Local_Transport)
LogHelper.Error($"Kafka链接错误{error}");
else if (error.Code == ErrorCode.Local_AllBrokersDown)
LogHelper.Error($"Kafka所有Broker都挂了{error}");
});
reportFuncWrapper.Debounce();
});
reportFuncWrapper.Debounce();
});
#if ProtoBuf
bb.WithProtobufNetDeserializer();
#elif NewtonsoftJson
bb.WithNewtonsoftJsonDeserializer(builder =>
builder.WithNullValueHandling(NullValueHandling.Ignore) // Ignores null values
.WithReferenceLoopHandling(ReferenceLoopHandling.Ignore) // Ignores reference loops
.WithContractResolver(new CamelCasePropertyNamesContractResolver()) // Camel case property names
);
#else
bb.WithJsonCoreDeserializer(bc =>
bc.WithReferenceHandler(ReferenceHandler.IgnoreCycles) // Handles circular references
.WithPropertyNamingPolicy(JsonNamingPolicy.CamelCase) // Uses camelCase naming
.WithDefaultIgnoreCondition(JsonIgnoreCondition.WhenWritingNull)
);
#endif
return (bb.Build() as IKafkaConsumer<TKey, TValue>)!;
var c = bb.Build();
c.Subscribe(subTopic);
return c;
});
}
private static void RegisterProducer<TKey, TValue>(IServiceCollection services, IConfiguration conf, string? defaultTopic = null)
{
services.AddSingleton<IKafkaProducer<TKey, TValue>>(sp =>
services.AddJsonSerializer<TKey>();
services.AddJsonSerializer<TValue>();
services.AddSingleton<IProducer<TKey, TValue>>(sp =>
{
var bb = new KafkaProducerBuilder<TKey, TValue>()
.WithProducerConfiguration(o =>
{
o.WithClientId(Dns.GetHostName())
.WithBootstrapServers(conf.GetValue<string>("Kafka:BootstrapServers"))
// 确保消息发送可靠
.WithAcks(Acks.All)
.WithEnableIdempotence(true) // 幂等发送
.WithMessageSendMaxRetries(conf.GetValue<int>("Kafka:Producer:MessageSendMaxRetries"))
.WithRequestTimeoutMs(conf.GetValue<int>("Kafka:Producer:RequestTimeoutMs"))
// 消息体大小
.WithCompressionType(conf.GetValue<CompressionType>("Kafka:Producer:CompressionType"))
.WithCompressionLevel(conf.GetValue<int>("Kafka:Producer:CompressionLevel")) //Gzip 0-9 LZ4 0-12 数值越大压缩效果越好cpu占用越多
// 消息发送速率控制(任意一条件先达到就发送)
.WithLingerMs(conf.GetValue<int>("Kafka:Producer:LingerMs"))
.WithBatchSize(conf.GetValue<int>("Kafka:Producer:BatchSize"))
.WithBatchNumMessages(conf.GetValue<int>("Kafka:Producer:BatchNumMessages"))
// 发送缓冲内存限制
.WithQueueBufferingMaxMessages(conf.GetValue<int>("Kafka:Producer:QueueBufferingMaxMessages"))
.WithQueueBufferingMaxKbytes(conf.GetValue<int>("Kafka:Producer:QueueBufferingMaxKbytes"))
// 分区器 一致性hash/随机
.WithPartitioner(conf.GetValue<Partitioner>("Kafka:Producer:Partitioner"))
.WithDefaultTopic(defaultTopic);
});
bb.WithLoggerFactory(sp.GetRequiredService<ILoggerFactory>());
DebounceHelper.DebounceWrapper? reportFuncWrapper = null;
bb.WithErrorHandler((producer, error) =>
{
if (error.IsFatal)
var bb = new ProducerBuilder<TKey, TValue>(new ProducerConfig()
{
LogHelper.Error($"Kafka致命错误${error}");
return;
}
ClientId = Dns.GetHostName(),
BootstrapServers = conf.GetValue<string>("Kafka:BootstrapServers"),
// 确保消息发送可靠
Acks = Acks.All,
EnableIdempotence = true,
MessageSendMaxRetries = conf.GetValue<int>("Kafka:Producer:MessageSendMaxRetries"),
RequestTimeoutMs = conf.GetValue<int>("Kafka:Producer:RequestTimeoutMs"),
// 消息体大小
CompressionType = conf.GetValue<CompressionType>("Kafka:Producer:CompressionType"),
CompressionLevel = conf.GetValue<int>("Kafka:Producer:CompressionLevel"),
// 消息发送速率控制(任意一条件先达到就发送)
LingerMs = conf.GetValue<int>("Kafka:Producer:LingerMs"),
BatchSize = conf.GetValue<int>("Kafka:Producer:BatchSize"),
BatchNumMessages = conf.GetValue<int>("Kafka:Producer:BatchNumMessages"),
// 发送缓冲内存限制
QueueBufferingMaxMessages = conf.GetValue<int>("Kafka:Producer:QueueBufferingMaxMessages"),
QueueBufferingMaxKbytes = conf.GetValue<int>("Kafka:Producer:QueueBufferingMaxKbytes"),
// 分区器 一致性hash/随机
Partitioner = conf.GetValue<Partitioner>("Kafka:Producer:Partitioner"),
})
.SetKeySerializer(sp.GetRequiredService<JsonCoreSerializer<TKey>>())
.SetValueSerializer(sp.GetRequiredService<JsonCoreSerializer<TValue>>())
.SetErrorHandler((_, error) =>
{
if (error.IsFatal)
{
LogHelper.Error($"Kafka致命错误${error}");
return;
}
// tips: 防抖以防止高频上报错误
reportFuncWrapper ??= DebounceHelper.WrapMethod(TimeSpan.FromSeconds(5), () =>
{
if (error.Code == ErrorCode.Local_Transport)
LogHelper.Error($"Kafka链接错误{error}");
else if (error.Code == ErrorCode.Local_AllBrokersDown)
LogHelper.Error($"Kafka所有Broker都挂了{error}");
// tips: 防抖以防止高频上报错误
reportFuncWrapper ??= DebounceHelper.WrapMethod(TimeSpan.FromSeconds(5), () =>
{
if (error.Code == ErrorCode.Local_Transport)
LogHelper.Error($"Kafka链接错误{error}");
else if (error.Code == ErrorCode.Local_AllBrokersDown)
LogHelper.Error($"Kafka所有Broker都挂了{error}");
});
reportFuncWrapper.Debounce();
});
reportFuncWrapper.Debounce();
});
#if ProtoBuf
// 2000w条消息数据包压缩从 2g -> 1g
bb.WithProtobufNetSerializer();
#elif NewtonsoftJson
bb.WithNewtonsoftJsonSerializer(builder =>
builder.WithNullValueHandling(NullValueHandling.Ignore) // Ignores null values
.WithReferenceLoopHandling(ReferenceLoopHandling.Ignore) // Ignores reference loops
.WithContractResolver(new CamelCasePropertyNamesContractResolver()) // Camel case property names
);
#else
bb.WithJsonCoreSerializer(bc =>
bc.WithReferenceHandler(ReferenceHandler.IgnoreCycles) // Handles circular references
.WithPropertyNamingPolicy(JsonNamingPolicy.CamelCase) // Uses camelCase naming
.WithDefaultIgnoreCondition(JsonIgnoreCondition.WhenWritingNull)
);
#endif
return (bb.Build() as IKafkaProducer<TKey, TValue>)!;
return bb.Build();
});
}
private static void AddJsonSerializer<T>(this IServiceCollection services, Action<JsonSerializerOptions>? setup = null)
{
services.TryAddSingleton(_ =>
{
var o = new JsonSerializerOptions();
setup?.Invoke(o);
return new JsonCoreSerializer<T>(o);
});
}
public static List<ConsumeResult<TKey, TValue>> ConsumeBatch<TKey, TValue>(this IConsumer<TKey, TValue> consumer, int batchSize, TimeSpan timeout)
{
using var cts = new CancellationTokenSource(timeout);
var list = new List<ConsumeResult<TKey, TValue>>();
while (list.Count < batchSize && !cts.IsCancellationRequested)
{
var consumeResult = consumer.Consume(100);
if (consumeResult != null)
list.Add(consumeResult);
}
return list;
}
public static void PrintKafkaLibVersion()
{
var loadSuccess = Library.Load(null);
Console.WriteLine($@"
+-------------------------------------------------------+
| |
| LibrdKafka |
| 加载:{loadSuccess},版本号:{Library.VersionString} |
| |
+-------------------------------------------------------+
");
}
}
internal sealed class JsonCoreSerializer<T> : ISerializer<T>, IDeserializer<T>
{
private readonly Encoding _encoding;
private readonly JsonSerializerOptions _options;
public JsonCoreSerializer(JsonSerializerOptions options)
{
_options = options ?? throw new ArgumentNullException(nameof(options));
_encoding = Encoding.UTF8;
}
public byte[] Serialize(T data, SerializationContext context)
{
if (data is null)
{
return null;
}
var json = JsonSerializer.Serialize(data, data.GetType(), _options);
var result = _encoding.GetBytes(json);
return result;
}
public T Deserialize(ReadOnlySpan<byte> data, bool isNull, SerializationContext context)
{
if (isNull)
{
return default;
}
#if NETSTANDARD2_0
var json = _encoding.GetString(data.ToArray());
#else
var json = _encoding.GetString(data);
#endif
var result = JsonSerializer.Deserialize<T>(json, _options);
return result;
}
}

View File

@ -1,5 +1,5 @@
using Confluent.Kafka;
using Confluent.Kafka.Core.Producer;
using Microsoft.Extensions.Configuration;
using SJZY.Track.Application.Services.Warning.Signals;
using SJZY.Track.Domain.Aggregates.TrackDetailsAggregate;
using Volo.Abp.DependencyInjection;
@ -9,27 +9,48 @@ namespace SJZY.Track.Application.Kafka;
/// <summary>
/// 预警信号发布者
/// </summary>
public sealed class WarningSignalPublisher : ITransientDependency
public sealed class WarningSignalPublisher : IDisposable, ISingletonDependency
{
private readonly IKafkaProducer<WarningSignalKey, WarningSignal> _producer;
private readonly IProducer<WarningSignalKey, WarningSignal> _producer;
private readonly IProducer<int, WarningSignal> _dlxProducer;
private readonly string env;
public WarningSignalPublisher(IKafkaProducer<WarningSignalKey, WarningSignal> producer)
public WarningSignalPublisher(IProducer<WarningSignalKey, WarningSignal> producer, IProducer<int, WarningSignal> dlxProducer, IConfiguration conf)
{
_producer = producer;
_dlxProducer = dlxProducer;
env = "-" + conf.GetValue<string>("Environment")!.ToLower();
}
public void PublishWarningSignal(WarningSignalType type, List<TrackDetailsIndex> tracks)
public void PublishSignal(WarningSignalType type, List<TrackDetailsIndex> tracks)
{
foreach (var track in tracks)
PublishWarningSignal(type, track);
PublishSignal(type, track);
}
public void PublishWarningSignal(WarningSignalType type, TrackDetailsIndex track)
public void PublishSignal(WarningSignalType type, TrackDetailsIndex track)
{
_producer.Produce(new Message<WarningSignalKey, WarningSignal>
_producer.Produce(GlobalConst.WarningKafkaTopic + env, new Message<WarningSignalKey, WarningSignal>
{
Key = new WarningSignalKey(type, track.OrderId),
Value = WarningSignal.FromTrack(track, type)
});
}
public Task PublishSignalAsync(Message<WarningSignalKey, WarningSignal> msg, CancellationToken ct = default)
{
return _producer.ProduceAsync(GlobalConst.WarningKafkaTopic + env, msg, ct);
}
public Task PublishDlxSignalAsync(Message<int, WarningSignal> message, CancellationToken ct = default)
{
return _dlxProducer.ProduceAsync(GlobalConst.WarningDLXKafkaTopic + env, message, ct);
}
public void Dispose()
{
_producer.Dispose();
_dlxProducer.Dispose();
}
}

View File

@ -9,10 +9,8 @@
<ItemGroup>
<PackageReference Include="BSI.Stack.Rabbit" Version="2.1.4" />
<PackageReference Include="Confluent.Kafka.Core" Version="1.0.0" />
<PackageReference Include="Confluent.Kafka.Core.Serialization.JsonCore" Version="1.0.0" />
<PackageReference Include="Confluent.Kafka.Core.Serialization.NewtonsoftJson" Version="1.0.0" />
<PackageReference Include="Confluent.Kafka.Core.Serialization.ProtobufNet" Version="1.0.0" />
<PackageReference Include="Confluent.Kafka" Version="2.10.0" />
<PackageReference Include="FeishuNetSdk" Version="2.4.6" />
<PackageReference Include="Hangfire.AspNetCore" Version="1.8.18" />
<PackageReference Include="Hangfire.Redis.StackExchange" Version="1.9.4" />
<PackageReference Include="Savorboard.CAP.InMemoryMessageQueue" Version="8.2.1"/>

View File

@ -32,6 +32,7 @@ public class BroadcastingService : IBroadcastingService
var data = new TrackChangeEto
{
EstimatedDeliveryDate = eto.EstimatedDeliveryDate,
DestinationTimeZone = eto.DestinationTimeZone,
TrackPlacePostcode = eto.TrackPlacePostcode,
TrackPlaceNation = eto.TrackPlaceNation,
TrackPlaceNationId = eto.TrackPlaceNationId,

View File

@ -5,12 +5,14 @@ using SJZY.Expand.ABP.Core.Logger;
using SJZY.Expand.ABP.Core.PublicMethords;
using SJZY.Track.Application.Contract.Dtos.TrackPush;
using SJZY.Track.Application.Contract.Services;
using SJZY.Track.Application.Helpers;
using SJZY.Track.Application.Localization;
using SJZY.Track.Application.Services.Warning.Handlers;
using SJZY.Track.Domain.Aggregates.OrderAggregate;
using SJZY.Track.Domain.Aggregates.TrackPushAggregate;
using SJZY.Track.Domain.Dtos.Common;
using SJZY.Track.Domain.Dtos.TrackPush;
using SJZY.Track.Domain.Enums;
using Volo.Abp.DependencyInjection;
namespace SJZY.Track.Application.Services.TrackPush;
@ -112,7 +114,7 @@ public class TrackPushService : ApplicationService, ISingletonDependency
/// <param name="input"></param>
/// <returns></returns>
[HttpPost("Edit")]
public async Task<bool> EditAsync([FromBody] EditTrackPushConfigInput input)
public async Task<string> EditAsync([FromBody] EditTrackPushConfigInput input)
{
if (input.Options != OptionTypeNoNull.删除)
{
@ -124,6 +126,14 @@ public class TrackPushService : ApplicationService, ISingletonDependency
throw new BusinessException(message: _stringLocalizer["IsTrackPushConfigAny"]);
}
var msg = string.Empty;
if (input.Options is OptionTypeNoNull.新增 or OptionTypeNoNull.修改)
{
var (success, message) = CheckDateTimeFormat(input.DateTimeFormat, input.DateTimeFormatType, input.TimezoneFormat);
if (!success) throw new BusinessException(message: message);
msg = message;
}
using var ts = TransactionScopeHelper.Create();
switch (input.Options)
@ -161,7 +171,7 @@ public class TrackPushService : ApplicationService, ISingletonDependency
}
ts.Complete();
return true;
return msg;
}
@ -346,4 +356,52 @@ public class TrackPushService : ApplicationService, ISingletonDependency
}
});
}
private static (bool Success, string Message) CheckDateTimeFormat(string? dateTimeFormat, int? dateTimeFormatType, string? timezoneFormat)
{
const int defaultTimeZone = 8;
var dateTime = DateTime.Now;
string dateStr;
try
{
if (dateTimeFormatType == (int) DateTimeFormatEnum.Timestamp)
{
dateStr = DateTimeHelper.ConvertToUnixTimestamp(dateTime).ToString();
return (true, $"格式化样例:{dateStr}");
}
if (dateTimeFormatType == (int) DateTimeFormatEnum.DateTimeAndTimezone)
{
// 没时间有时区,出错
if (string.IsNullOrWhiteSpace(dateTimeFormat) && !string.IsNullOrWhiteSpace(timezoneFormat))
return (false, "如果填写了时区格式,那么时间格式是必须的");
// 只有时间
if (!string.IsNullOrWhiteSpace(dateTimeFormat) && string.IsNullOrWhiteSpace(timezoneFormat))
{
var datetimeStr = DateTimeHelper.FormatDateTime(dateTime, dateTimeFormat);
return (true, $"格式化样例:{datetimeStr}");
}
// 时间和时区同时存在
if (!string.IsNullOrWhiteSpace(dateTimeFormat) && !string.IsNullOrWhiteSpace(timezoneFormat))
{
var zoneStr = DateTimeHelper.TimezoneFormat(defaultTimeZone, timezoneFormat);
var datetimeStr = DateTimeHelper.FormatDateTime(dateTime, dateTimeFormat);
dateStr = $"{datetimeStr} {zoneStr}";
return (true, $"格式化样例:{dateStr}");
}
}
}
catch (Exception e)
{
return (false, $"时间格式错误请检查:{e.Message}");
}
// 默认情况
dateTimeFormat ??= "yyyy-MM-ddTHH:mm:ss.fffK";
dateStr = dateTime.ToString(dateTimeFormat);
return (true, $"格式化样例:{dateStr}");
}
}

View File

@ -139,7 +139,7 @@ public class SjzyTrackApplicationModule : AbpModule
Console.WriteLine("-- 当前为VS本地环境,用内存存储和内存队列");
}
Action<RabbitOptions> action = options =>
Action<RabbitOptions> action = options =>
{
options.ConnectionString =
$"amqp://{configuration["RabbitMQ:UserName"]}:{configuration["RabbitMQ:Password"]}@{configuration["RabbitMQ:HostName"]}:{configuration["RabbitMQ:Port"]}";
@ -203,12 +203,19 @@ public class SjzyTrackApplicationModule : AbpModule
$",abortConnect=false";
// 注册预警计算服务
if (_BUGOptions.EnablePref)
context.Services.AddWarningCalcServices(configuration);
context.Services.AddWarningCalcServices(configuration);
// 注册预警任务后台服务
context.Services.AddHostedService<WarningTaskComponentBackgroundService>();
// 注册飞书服务(用于预警通知)
context.Services.AddFeishuNetSdk(o =>
{
o.AppId = configuration.GetValue<string>("FeiShu:AppId")!;
o.AppSecret = configuration.GetValue<string>("FeiShu:AppSecret")!;
o.EnableLogging = configuration.GetValue<bool>("FeiShu:EnableLogging");
});
// 注册HangFire服务
context.Services.Configure<TrackWarningTaskOptions>(configuration.GetSection("WarningTask"));
context.Services.AddHangfire(o =>

View File

@ -1,6 +0,0 @@
namespace SJZY.Track.Application;
public static class _BUGOptions
{
public static bool EnablePref = false;
}

View File

@ -0,0 +1,7 @@
using SJZY.Track.Domain.VEntities;
namespace SJZY.Track.Domain.Queries;
public interface IVFsUserQueries : IBasicQuery<VFSUserEntity>
{
}

View File

@ -0,0 +1,65 @@
namespace SJZY.Track.Domain.VEntities;
/// <summary>
/// 飞书用户表,用户中心用户关联飞书用户
/// </summary>
[SugarTable("v_fs_user")]
public class VFSUserEntity : IEntity
{
[SugarColumn(ColumnName = "Id", IsPrimaryKey = true, IsIdentity = true)]
public int Id { get; set; }
/// <summary>
/// 用户中心id
/// </summary>
[SugarColumn(ColumnName = "user_center_id")]
public int UserCenterId { get; set; }
/// <summary>
/// 用户名
/// </summary>
[SugarColumn(ColumnName = "user_name")]
public string UserName { get; set; }
/// <summary>
/// 用户手机号
/// </summary>
[SugarColumn(ColumnName = "mobile")]
public string Mobile { get; set; }
/// <summary>
/// 飞书 union id
/// </summary>
[SugarColumn(ColumnName = "union_id")]
public string UnionId { get; set; }
/// <summary>
/// 飞书 user id
/// </summary>
[SugarColumn(ColumnName = "fs_employee_id")]
public string FsEmployeeId { get; set; }
/// <summary>
/// 飞书头像
/// </summary>
[SugarColumn(ColumnName = "avatar")]
public string Avatar { get; set; }
/// <summary>
/// 自建的飞书登录应用下的OpenId
/// </summary>
[SugarColumn(ColumnName = "fsloginApp_openid")]
public string FsLoginAppOpenId { get; set; }
/// <summary>
/// 创建时间
/// </summary>
[SugarColumn(ColumnName = "create_time")]
public DateTime CreateTime { get; set; }
/// <summary>
/// 更新时间
/// </summary>
[SugarColumn(ColumnName = "update_time")]
public DateTime UpdateTime { get; set; }
}

View File

@ -0,0 +1,9 @@
using SJZY.Track.Domain.Queries;
using SJZY.Track.Domain.VEntities;
namespace SJZY.Track.Repository.Queries;
public class VFsUserQueries: BasicQuery<VFSUserEntity>, IVFsUserQueries, ITransientDependency
{
}