using DotNetCore.CAP; using Fake.DependencyInjection; using InterfaceForward.Domain.Shared.Helpers; using Microsoft.Extensions.Logging; using TrackCenter.Application.Events; using TrackCenter.Application.Kafka; using TrackCenter.Application.Queries; using TrackCenter.Application.Services.Broadcasting; using TrackCenter.Application.Services.Warning.Signals; using TrackCenter.Domain.Aggregates.OrderAggregate; using TrackCenter.Domain.Aggregates.TrackDetailsAggregate; using TrackCenter.Domain.Dtos.TrackContents; using TrackCenter.Domain.Enums; using TrackCenter.Domain.Extensions; using TrackCenter.Repository.Aggregates.OrderAggregate; using OrderIdentifier = TrackCenter.Application.Events.OrderIdentifier; namespace TrackCenter.Application.CAP.EventHandlers; public class TrackUpsertEventHandler : ITransientDependency, ICapSubscribe { private readonly ICapPublisher _capPublisher; private readonly IOrderRepository _orderRepository; private readonly ProvideQueries _provideQueries; private readonly ITrackDetailsEsRepository _trackDetailsEsRepository; private readonly BroadcastingService _broadcastingService; private readonly ILogger _logger; private readonly IOrderOperationRecordEsRepository _orderOperationRecordEsRepository; private readonly WarningSignalPublisher _warningSignalPublisher; public TrackUpsertEventHandler( IOrderRepository orderRepository, ICapPublisher capPublisher, ProvideQueries provideQueries, ITrackDetailsEsRepository trackDetailsEsRepository, IOrderOperationRecordEsRepository orderOperationRecordEsRepository, BroadcastingService broadcastingService, ILogger logger, WarningSignalPublisher warningSignalPublisher ) { _orderRepository = orderRepository; _capPublisher = capPublisher; _provideQueries = provideQueries; _trackDetailsEsRepository = trackDetailsEsRepository; _orderOperationRecordEsRepository = orderOperationRecordEsRepository; _broadcastingService = broadcastingService; _logger = logger; _warningSignalPublisher = warningSignalPublisher; } [CapSubscribe(GlobalConst.TrackUpsertSubscribe, Group = GlobalConst.TrackUpsertSubscribe)] [CapSubscribe(GlobalConst.TrackUpsertForRefreshSubscribe, Group = GlobalConst.TrackUpsertForRefreshSubscribe)] public async Task ConsumerAsync(TrackUpsertEvent @event) { if (!CanDelete(@event)) return; var canNextOrderIds = new List(); var content = await HandleTrackAsync(@event, canNextOrderIds); // 处理广播事件 if (content != null && (content.IsToMq || @event.IsToMq)) _ = EnsureWriteAfterPublicTopicAsync(@event, content.IsExternal, content.SubSystemCode, content.TrackStatusCode); if (canNextOrderIds.Count == 0) return; // 根据订单关联关系异步向下刷(异步) await DownByOrderLink(@event, canNextOrderIds); } private async Task DownByOrderLink(TrackUpsertEvent @event, List canNextOrderIds) { // 根据订单关联关系异步向下刷(异步) var linkedOrders = await _orderRepository.GetLinkedOrdersAsync(canNextOrderIds.ToArray()); foreach (var order in @event.OrderIdentifiers) { // 理论上小包在这不会分组 foreach (var group in linkedOrders.Where(x => x.OwnerOrderId == order.OrderId) .GroupBy(x => new { x.SubSystemId, x.CustomId // 这里按客户分组是为了后续的推送方便 })) { var nextEvent = @event.DeepCopy(); nextEvent.Visited.AddRange(canNextOrderIds); nextEvent.SubSystemId = group.Key.SubSystemId; nextEvent.CustomId = group.Key.CustomId; nextEvent.TrackRefreshData = new TrackRefreshStruct { ServiceProviderCode = @event.TrackRefreshData.ServiceProviderCode, OriginTrackContent = @event.TrackRefreshData.OriginTrackContent, OriginTrackDataHash = @event.TrackRefreshData.OriginTrackDataHash, }; // 不同子系统需要做内容映射 if (@event.SubSystemId != nextEvent.SubSystemId) { var mapCode = (await _provideQueries.MapTrackCodesAsync(@event.SubSystemId, nextEvent.SubSystemId, @event.TrackCode)) .FirstOrDefault(); if (mapCode == null) { // 跨子系统没做映射的轨迹不处理 continue; } nextEvent.TrackCode = mapCode; } var identifiers = group.Select(x => new OrderIdentifier { OrderId = x.OrderId, OrderNo = x.OrderNo, OrderType = x.OrderType, }).ToList(); var idx = 0; while (idx < identifiers.Count) { var subEvent = nextEvent.DeepCopy(); var cnt = idx + GlobalConst.ChunkSize > identifiers.Count ? identifiers.Count - idx : GlobalConst.ChunkSize; subEvent.OrderIdentifiers = identifiers.GetRange(idx, cnt).ToArray(); idx += GlobalConst.ChunkSize; await _capPublisher.PublishAsync(GlobalConst.TrackUpsertSubscribe, subEvent); } } } } private static bool CanDelete(TrackUpsertEvent @event) { // 系统创建的轨迹不允许修改 if (@event.Visited.IsNullOrEmpty() && @event is { OperationMan: GlobalConst.SystemUser, IsAdd: false }) { LogHelper.Warn($"系统创建的轨迹不允许被修改:{@event}", true); return false; } var orderIds = @event.OrderIdentifiers.Select(x => x.OrderId).ToArray(); var dup = @event.Visited.Intersect(orderIds).ToArray(); if (dup.Any()) { LogHelper.Warn($"{(@event.IsAdd ? "添加" : "修改")}轨迹时,发现循环依赖:{dup.JoinAsString(",")}", true); return false; } return true; } private async Task HandleTrackAsync(TrackUpsertEvent @event, List canNextOrderIds) { var trackContent = await _provideQueries.GetTrackContentAsync(@event.SubSystemId, @event.TrackCode); if (trackContent == null) { // 找不到轨迹内容,直接结束 LogHelper.Warn($"找不到轨迹内容:{@event}", true); return null; } //#7001:未来潜在需求,当轨迹内容匹配预设,记录一个标记,保证下刷过程都是使用预设内容 //虽然在引发事件之前做了判断,引发之后并不一定立即处理,在此过程中轨迹内容仍然可能被修改,所以要二次进行判断。 //通过来源指示是否为手动操作 var manualOperation = @event.Source == (int) TrackSourceEnum.Artificial; //轨迹内容标识不允许手动创建的 if (manualOperation && @event.IsAdd && trackContent.IsAllowManuallyCreate == false) return trackContent; //轨迹内容标识不允许手动更新的 if (manualOperation && !@event.IsAdd && trackContent.IsAllowManuallyUpdate == false) return trackContent; // k:[orderId] v:[k:EsId,v:Version] var orderIdEsIdVersionDict = new Dictionary>(); var insertTracks = await UpsertTrackAsync(@event, canNextOrderIds, trackContent, orderIdEsIdVersionDict); if (canNextOrderIds.Count == 0) return trackContent; if (insertTracks.Any()) { // 批量add to es tips:注意副作用填充版本号 await _trackDetailsEsRepository.AddManyAsync(insertTracks); // es新增后填充版本号字典 insertTracks.ForEach(t => orderIdEsIdVersionDict.Add(t.OrderId, KeyValuePair.Create(t.Id, t.Version))); //批量写入订单操作日志 _ = _orderOperationRecordEsRepository.AddManyAsync(insertTracks.Select(t => BuildOrderOperationRecord(@event, t, new OrderIdentifier { OrderNo = t.OrderNo, OrderId = t.OrderId })).ToList()); // 添加轨迹触发订单拉取状态更新 await _orderRepository.UpdatePullStatusToSilenceAsync(insertTracks.Select(x => x.OrderId).ToList(), @event.TrackCode); // todo:引发预警计算 改MQ批量+分流 WarningCalc_AddedTrack(insertTracks); // 添加轨迹触发预警计算(异步)todo:弃用 // await _capPublisher.PublishAsync(GlobalConst.TrackWarningCalcSubscribe, new TrackWarningCalcEvent // { // SubSystemId = @event.SubSystemId, // CustomId = @event.CustomId, // OrderIds = insertTracks.Select(x => x.OrderId).ToArray(), // TrackStatusId = trackContent.TrackStatusId, // TrackTime = @event.TrackTime, // }); } // 触发订单主表更新事件(异步) await _capPublisher.PublishAsync(GlobalConst.TrackChangeAfterUpdateOrderSubscribe, new TrackChangeAfterUpdateOrderEvent { OrderIds = canNextOrderIds.ToArray(), IsCompleted = trackContent.IsCompleted, TrackStatus = trackContent.TrackStatus, TrackContent = trackContent.TrackContent, TrackEnContent = trackContent.TrackEnContent, TrackPlace = @event.TrackPlace, TrackTime = @event.NewTrackTime ?? @event.TrackTime, TrackTimeZone = @event.TrackTimeZone, DestinationTimeZone = @event.DestinationTimeZone, EstimatedDeliveryDate = @event.EstimatedDeliveryDate }); // 触发轨迹推送事件(异步) // tips:同一个轨迹内容,多个订单多条轨迹 var trackPushEvent = new TrackPushEvent { OptionType = @event.IsAdd ? OptionType.新增 : OptionType.修改, SubSystemId = @event.SubSystemId, SubSystemCode = trackContent.SubSystemCode, CustomId = @event.CustomId, OrderIdEsIdVersionDict = orderIdEsIdVersionDict, TrackCode = @event.TrackCode, SystemPushInfo = new SystemPushStruct { TrackStatusId = trackContent.TrackStatusId, TrackStatus = trackContent.TrackStatus, TrackContentId = trackContent.TrackContentId, TrackContent = trackContent.TrackContent, TrackPlace = @event.TrackPlace, TrackTime = @event.NewTrackTime ?? @event.TrackTime, TrackTimeZone = @event.TrackTimeZone, CompareFields = @event.CompareFields, Extra = @event.NewExtra ?? @event.Extra }, ForcedPush = @event.ForcedPush, IsToMq = (trackContent.IsToMq || @event.IsToMq) }; await _capPublisher.PublishAsync(GlobalConst.TrackPushSubscribe, trackPushEvent); return trackContent; } private async Task> UpsertTrackAsync( TrackUpsertEvent @event, List canNextOrderIds, TrackContentVO trackContent, Dictionary> orderIdEsIdVersionDict ) { var orderIds = @event.OrderIdentifiers.Select(x => x.OrderId).ToArray(); // important:轨迹code相同,时间、发生地不同的,可能存在多条!! var tracks = await _trackDetailsEsRepository.GetListAsync(orderIds, @event.TrackCode, true); var insertTracks = new List(); var updateTracks = new List(); var operationRecords = new List(); var traceLog = new Dictionary { { "@event", @event.ToJson() } }; foreach (var order in @event.OrderIdentifiers) { // fast path: by update with es id case if (!@event.IsAdd && !string.IsNullOrWhiteSpace(@event.EsId)) { var trackById = tracks.FirstOrDefault(x => x.Id == @event.EsId); if (trackById != null) { updateTracks.Add(BuiltUpdateTrack(@event, trackById, order, trackContent)); } continue; } var existTracks = tracks.Where(x => x.OrderId == order.OrderId).ToList(); // tips:如果没传时区则使用+8兜底进行过滤 var timezone = Math.Abs(@event.TrackTimeZone - GlobalConst.NoneTrackTimeZone) < 0.01f ? 8.0f : @event.TrackTimeZone; // todo-remove:为了照顾FBA老逻辑,暂时特殊处理一下 var trackFirst = tracks.FirstOrDefault(); if (trackFirst?.SubSystemId == 9517) { existTracks = @event.IsAdd ? tracks.Where(x => x.TrackTime == (@event.NewTrackTime ?? @event.TrackTime)).ToList() : tracks.Where(x => x.TrackTime == @event.TrackTime // 修改了时间,要校验修改后的时间是否存在 || (@event.NewTrackTime != null && x.TrackTime == @event.NewTrackTime) ).ToList(); } else { // 根据时间过滤 existTracks = @event.IsAdd ? existTracks.Where(x => x.GetUtc0TrackTime() == (@event.NewTrackTime ?? @event.TrackTime).Subtract(TimeSpan.FromHours(timezone))).ToList() : existTracks.Where(x => x.GetUtc0TrackTime() == @event.TrackTime.Subtract(TimeSpan.FromHours(timezone)) // 修改了时间,要校验修改后的时间是否存在 || (@event.NewTrackTime != null && x.GetUtc0TrackTime() == @event.NewTrackTime.Value.Subtract(TimeSpan.FromHours(timezone))) ).ToList(); } // 根据扩展信息过滤 existTracks = @event.IsAdd ? existTracks.Where(x => x.Extra == (@event.NewExtra ?? @event.Extra)).ToList() : existTracks.Where(x => x.Extra == @event.Extra // 修改了extra,要校验修改后的extra是否存在 || (!string.IsNullOrWhiteSpace(@event.NewExtra) && x.Extra == @event.NewExtra)).ToList(); if (@event.IsAdd) { if (existTracks.Count > 0) { _logger.LogInformation("订单已经存在不允许重复的轨迹,不允许重复添加:{@event}", @event); traceLog.Add($"OrderNo-{order.OrderNo}", "订单已经存在不允许重复的轨迹,不允许重复添加"); continue; } insertTracks.Add(BuiltNewTrack(@event, order, trackContent)); } else { // 不存在不让改 if (existTracks.Count == 0) { _logger.LogInformation("订单不存在轨迹,不允许修改:{@event}", @event); traceLog.Add($"OrderNo-{order.OrderNo}", "订单不存在轨迹,不允许修改"); continue; } if (existTracks.Count > 1) { _logger.LogInformation("订单存在多条轨迹不允许修改:{@event}", @event); traceLog.Add($"OrderNo-{order.OrderNo}", "订单存在多条轨迹不允许修改"); continue; } var track = existTracks.First(); updateTracks.Add(BuiltUpdateTrack(@event, track, order, trackContent)); operationRecords.Add(BuildOrderOperationRecord(@event, track, order)); } canNextOrderIds.Add(order.OrderId); } ActivityHelper.StartSpanActivity("UpsertTrackAsync", traceLog); if (updateTracks.Any()) { // 批量差异化更新轨迹 await _trackDetailsEsRepository.BulkUpdateAsync(updateTracks); // 获取更新后的版本号 updateTracks.ForEach(x => orderIdEsIdVersionDict.Add(x.OrderId, KeyValuePair.Create(x.Id, x.Version))); // 批量写入操作日志 _ = _orderOperationRecordEsRepository.AddManyAsync(operationRecords); // todo:引发预警计算 改MQ批量+分流 WarningCalc_UpdatedTrack(updateTracks); } return insertTracks; } private static TrackDetailsIndex BuiltUpdateTrack(TrackUpsertEvent @event, TrackDetailsIndex track, OrderIdentifier order, TrackContentVO trackContent) { //外部轨迹,轨迹发生地,轨迹发生时间,轨迹发生时区,不允许更改 if (track.Source != (int) TrackSourceEnum.ExternalServiceProvide) { track.TrackPlace = @event.TrackPlace; track.TrackTime = @event.NewTrackTime ?? @event.TrackTime; track.TrackTimeZone = @event.TrackTimeZone; } track.ZeroTrackTime = Math.Abs(track.TrackTimeZone - GlobalConst.NoneTrackTimeZone) < 1 ? track.TrackTime : track.TrackTime.Subtract(TimeSpan.FromHours(track.TrackTimeZone)); track.UpdateUser = @event.OperationMan ?? GlobalConst.SystemUser; track.UpdateTime = DateTime.Now; //允许二次修改,在修改了轨迹内容后进行修改轨迹操作,如果输入的轨迹内容则以输入为准 //跨子系统? track.TrackContent = string.IsNullOrWhiteSpace(@event.Content) ? trackContent.TrackContent : @event.Content; track.TrackEnContent = string.IsNullOrWhiteSpace(@event.EnStatus) ? trackContent.TrackEnContent : @event.EnStatus; //当传入的IsExternal为空,则使用轨迹内容中的IsExternal track.IsExternal = @event.IsExternal ?? trackContent.IsExternal; track.Remarks = @event.Remarks ?? GlobalConst.EmptyString; track.Files = @event.Files?.Select(x => new FileItem { OssKey = x.OssKey, FileName = x.FileName }).ToList(); track.TrackPlaceCity = @event.TrackPlaceCity ?? GlobalConst.EmptyString; track.TrackPlaceCityId = @event.TrackPlaceCityId ?? GlobalConst.NotExistId; track.TrackPlaceProvince = @event.TrackPlaceProvince ?? GlobalConst.EmptyString; track.TrackPlaceProvinceId = @event.TrackPlaceProvinceId ?? GlobalConst.NotExistId; track.TrackPlaceNation = @event.TrackPlaceNation ?? GlobalConst.EmptyString; track.TrackPlaceNationId = @event.TrackPlaceNationId ?? GlobalConst.NotExistId; track.TrackPlacePostcode = @event.TrackPlacePostcode ?? GlobalConst.EmptyString; track.DestinationTimeZone = @event.DestinationTimeZone; track.EstimatedDeliveryDate = @event.EstimatedDeliveryDate; track.Extra = @event.NewExtra ?? @event.Extra; return track; } private static TrackDetailsIndex BuiltNewTrack(TrackUpsertEvent @event, OrderIdentifier order, TrackContentVO trackContent) { var track = new TrackDetailsIndex { SubSystemId = @event.SubSystemId, OrderId = order.OrderId, OrderNo = order.OrderNo, OrderType = order.OrderType, TrackCode = @event.TrackCode, TrackPlace = @event.TrackPlace, TrackTime = @event.NewTrackTime ?? @event.TrackTime, TrackTimeZone = @event.TrackTimeZone, ServiceProviderCode = @event.TrackRefreshData.ServiceProviderCode ?? GlobalConst.EmptyString, OriginTrackContent = @event.TrackRefreshData.OriginTrackContent ?? GlobalConst.EmptyString, OriginTrackDataHash = @event.TrackRefreshData.OriginTrackDataHash ?? GlobalConst.EmptyString, CreateUser = @event.OperationMan ?? GlobalConst.SystemUser, CreateTime = DateTime.Now, //来源轨迹内容,第一次新增的时候使用轨迹内容表提供的 TrackStatusId = trackContent.TrackStatusId, TrackStatus = trackContent.TrackStatus, TrackStatusCode = trackContent.TrackStatusCode, //跨子系统? TrackContentId = trackContent.TrackContentId, TrackContent = string.IsNullOrWhiteSpace(@event.Content) ? trackContent.TrackContent : @event.Content, TrackEnContent = string.IsNullOrWhiteSpace(@event.EnStatus) ? trackContent.TrackEnContent : @event.EnStatus, //当传入的IsExternal为空,则使用轨迹内容中的IsExternal IsExternal = @event.IsExternal ?? trackContent.IsExternal, Source = @event.Source, Remarks = @event.Remarks ?? GlobalConst.EmptyString, Files = @event.Files?.Select(x => new FileItem { OssKey = x.OssKey, FileName = x.FileName }).ToList(), TrackPlaceCity = @event.TrackPlaceCity ?? GlobalConst.EmptyString, TrackPlaceCityId = @event.TrackPlaceCityId ?? GlobalConst.NotExistId, TrackPlaceProvince = @event.TrackPlaceProvince ?? GlobalConst.EmptyString, TrackPlaceProvinceId = @event.TrackPlaceProvinceId ?? GlobalConst.NotExistId, TrackPlaceNation = @event.TrackPlaceNation ?? GlobalConst.EmptyString, TrackPlaceNationId = @event.TrackPlaceNationId ?? GlobalConst.NotExistId, TrackPlacePostcode = @event.TrackPlacePostcode ?? GlobalConst.EmptyString, DestinationTimeZone = @event.DestinationTimeZone, EstimatedDeliveryDate = @event.EstimatedDeliveryDate, Extra = @event.NewExtra ?? @event.Extra, }; track.ZeroTrackTime = Math.Abs(track.TrackTimeZone - GlobalConst.NoneTrackTimeZone) < 1 ? track.TrackTime : track.TrackTime.Subtract(TimeSpan.FromHours(track.TrackTimeZone)); return track; } private static OrderOperationRecordIndex BuildOrderOperationRecord(TrackUpsertEvent @event, TrackDetailsIndex track, OrderIdentifier order) { return new OrderOperationRecordIndex { OrderId = order.OrderId, OrderNo = order.OrderNo, OptionType = @event.IsAdd ? OrderOperationType.创建轨迹.ToString() : OrderOperationType.修改轨迹.ToString(), Content = track.ToJson(), CreateUserId = @event.OperationManId, CreateUserName = @event.OperationMan ?? "", CreateTime = DateTime.Now }; } private async Task EnsureWriteAfterPublicTopicAsync(TrackUpsertEvent @event, bool contentIsExternal, string subSystemCode, string statusCode) { //理想情况在写入之后1s会刷盘 https://www.elastic.co/guide/en/elasticsearch/reference/master/indices-refresh.html //增加一个版本号来确定是否真的变更了 //没有刷盘成功事件回调 //方案1:!!【轮询等待静默刷盘】查出版本号确实发生了变更在发送广播 //方案2:考虑强制刷盘(对es压力大)后发送 //方案3:采用一个较大延时,不查询直接发 await Task.Delay(1000); _broadcastingService.TrackChangePublicTopic(@event, contentIsExternal, subSystemCode, statusCode); } private void WarningCalc_AddedTrack(List tracks) { _warningSignalPublisher.PublishSignal(WarningSignalType.轨迹新增, tracks); } private void WarningCalc_UpdatedTrack(List tracks) { _warningSignalPublisher.PublishSignal(WarningSignalType.轨迹更新, tracks); } }