using DotNetCore.CAP; using Microsoft.AspNetCore.Http; using SJZY.Track.Application.Contract.Dtos.ProvideServices; using SJZY.Track.Application.Contract.Events; using SJZY.Track.Application.Contract.Services.ProvideServices; using SJZY.Track.Domain.Aggregates.OrderAggregate; using SJZY.Track.Domain.Aggregates.TrackContentAggregate; using SJZY.Track.Domain.Aggregates.TrackDetailsAggregate; using SJZY.Track.Domain.Aggregates.TrackTemplateAggregate; using SJZY.Track.Domain.Enums; using SJZY.Track.Domain.Extensions; using SJZY.Track.Domain.Queries; using SJZY.UserCenter.HttpApi.Services; using SJZY.UserCenter.HttpApi.Services.User; using Volo.Abp.Http.Client.DynamicProxying; namespace SJZY.Track.Application.Services.ProvideServices; /// /// 轨迹服务 /// [ApiExplorerSettings(GroupName = "提供接口")] [Route("/ProviderService")] public partial class TrackService : ApplicationService, ITrackService { private readonly ICapPublisher _capPublisher; private readonly IOrderRepository _orderRepository; private readonly IHttpClientProxy _requestCurrentUserService; private readonly ITrackContentRepository _trackContentRepository; public TrackService(IOrderRepository orderRepository, ICapPublisher capPublisher, ITrackDetailsEsRepository trackDetailsEsRepository, ITrackContentRepository trackContentRepository, IHttpClientProxy requestCurrentUserService, ITrackTemplateRepository trackTemplateRepository, IHttpContextAccessor contextAccessor, IVSystemQueries vSystemQueries) { _orderRepository = orderRepository; _capPublisher = capPublisher; _trackContentRepository = trackContentRepository; _requestCurrentUserService = requestCurrentUserService; _vSystemQueries = vSystemQueries; _contextAccessor = contextAccessor; _trackTemplateRepository = trackTemplateRepository; _trackDetailsEsRepository = trackDetailsEsRepository; } /// /// 删除轨迹 /// /// /// /// [HttpPost("Track/Delete")] public async Task DeleteTrackAsync(DeleteTrackInput input) { var orderId = await _orderRepository.GetFirstAsync( x => x.SubSystemId == input.SubSystemId && x.OrderType == input.OrderType && x.OrderNo == input.OrderNo, x => x.Id); if (orderId == null) { throw new BusinessException(message: $"订单:{input.OrderNo}不存在"); } var userInfo = await _requestCurrentUserService.Service.GetCurrentUserBasicInfoAsync(); if (userInfo.Code != GlobalConst.SuccessCode || userInfo.Data is null) { //TODO: log || ex throw new BusinessException(message: $"调用用户中心,获取用户信息失败:{userInfo.Msg}"); } var name = userInfo.Data.UserName; List tracks; bool existsExternal; var esIds = input.EsIds?.Distinct().ToArray(); //优先使用esId if (esIds != null && esIds.Length > 0) { tracks = await _trackDetailsEsRepository.GetDataByIds(esIds); existsExternal = tracks.Any(x => x.Source == (int) TrackSourceEnum.ExternalServiceProvide); } else { var trackCodes = input.TrackIdentifiers?.Select(x => x.TrackCode).ToList(); var trackIds = input.TrackIdentifiers?.Select(y => $"{y.TrackCode}{y.TrackTime}").ToHashSet(); //没有任何需要操作的轨迹 if (trackIds == null || trackIds.Count == 0) { throw new BusinessException(message: "没有任何轨迹要删除,请检查输入参数"); } //删除轨迹约束,只要其中有一条来源是外部的就立即返回错误 tracks = await _trackDetailsEsRepository.GetListAsync(new[] { orderId }, trackCodes); //检查删除的轨迹中是否包含外部轨迹 existsExternal = tracks .Where(x => x.Source == (int) TrackSourceEnum.ExternalServiceProvide) .WhereIf(!input.CompareFields.IsNullOrWhiteSpace(), y => { if (string.IsNullOrWhiteSpace(y.Extra)) { throw new BusinessException(message: "轨迹extra为空无法使用比较字段比较,请检查轨迹"); } var extraDict = System.Text.Json.JsonSerializer.Deserialize>(y.Extra); if (extraDict!.TryGetValue(input.CompareFields!, out var compareValueJsonElement)) return TextJsonHelper.EqJsonElementAndObject(compareValueJsonElement, input.CompareValue); return false; }) .Select(x => $"{x.TrackCode}{x.TrackTime}") .Any(x => trackIds.Contains(x)); } if (existsExternal) { throw new BusinessException(message: "存在来源于外部服务商的轨迹,不允许删除"); } if (tracks.Count == 0) { throw new BusinessException(message: "没有任何轨迹要删除,请检查输入参数"); } // 删除轨迹 await _capPublisher.PublishAsync(GlobalConst.TrackDeleteSubscribe, new TrackDeleteEvent { EsIds = input.EsIds?.ToArray(), OrderIds = new[] { orderId }, CompareFields = input.CompareFields, CompareValue = input.CompareValue, Source = input.Source, TrackIdentifiers = input.TrackIdentifiers, SubSystemId = input.SubSystemId, OperationMan = name }); return new UnifyResultDto(true, true); } /// /// 根据esid删除轨迹内容 /// /// /// [HttpPost("Track/BatchDeleteByEsId")] public async Task BatchDeleteTrackByEsIdAsync(BatchDeleteTrackByEsIdInput input) { var tracks = await _trackDetailsEsRepository.GetDataByIds(input.TrackContentEsIds.ToArray()); if (tracks == null || tracks.Count == 0) return new UnifyResultDto(false, "未找到指定id下的轨迹内容"); if (tracks.Any(m => m.Source == TrackSourceEnum.ExternalServiceProvide.GetHashCode())) return new UnifyResultDto(false, "存在来源于外部服务商的轨迹,不允许删除"); if (tracks.GroupBy(m => m.SubSystemId).Count() > 1) return new UnifyResultDto(false, "暂不支持跨子系统批量删除"); // 获取用户name var userInfo = await _requestCurrentUserService.Service.GetCurrentUserBasicInfoAsync(); if (userInfo.Code != GlobalConst.SuccessCode || userInfo.Data is null) { throw new BusinessException(message: $"调用用户中心,获取用户信息失败:{userInfo.Msg}"); } // 删除轨迹 var currentUser = CurrentUserRequest.CurrentUser; var userName = userInfo.Data.UserName; if (string.IsNullOrEmpty(userName)) userName = (await _orderRepository.GetUserById(currentUser.UserId))?.Name; var @event = new TrackDeleteEvent { EsIds = input.TrackContentEsIds.ToArray(), SubSystemId = tracks.First().SubSystemId, Source = input.Source, OperationMan = userName, OperationManId = currentUser.UserId }; await _capPublisher.PublishAsync(GlobalConst.TrackDeleteSubscribe, @event); return new UnifyResultDto(true); } /// /// 批量删除轨迹 /// /// /// /// [HttpPost("Track/BatchDelete")] public async Task BatchDeleteTrackAsync(BatchDeleteTrackInput input) { input.OrderNos = input.OrderNos.Where(x => !x.IsNullOrWhiteSpace()).Distinct().ToList(); // 单号去重、去空 var orderIds = await _orderRepository.GetListAsync(x => x.SubSystemId == input.SubSystemId && x.OrderType == input.OrderType && input.OrderNos.Contains(x.OrderNo) , x => x.Id); if (orderIds.Count == 0) { throw new BusinessException(message: "订单一个都不存在"); } // 获取用户name var userInfo = await _requestCurrentUserService.Service.GetCurrentUserBasicInfoAsync(); if (userInfo.Code != GlobalConst.SuccessCode || userInfo.Data is null) { throw new BusinessException(message: $"调用用户中心,获取用户信息失败:{userInfo.Msg}"); } //删除轨迹约束,只要其中有一条来源是外部的就立即返回错误 var tracks = await _trackDetailsEsRepository .GetListAsync(orderIds.ToArray(), input.TrackIdentifiers.Select(x => x.TrackCode).ToList(), TrackSourceEnum.ExternalServiceProvide); //当要删除的轨迹标识出现交叉则不允许删除 var intersect = tracks .WhereIf(!input.CompareFields.IsNullOrWhiteSpace(), y => { if (string.IsNullOrWhiteSpace(y.Extra)) { throw new BusinessException(message: "轨迹extra为空无法使用比较字段比较,请检查轨迹"); } var extraDict = TextJsonHelper.DeserializeToDictionary(y.Extra); if (extraDict!.TryGetValue(input.CompareFields!, out var compareValueJsonElement)) return TextJsonHelper.EqJsonElementAndObject(compareValueJsonElement, input.CompareValue); return false; }) .Select(x => $"{x.TrackCode}{x.TrackTime}") .Intersect(input.TrackIdentifiers.Select(y => $"{y.TrackCode}{y.TrackTime}")).ToList(); if (intersect.Count > 0) { throw new BusinessException(message: "存在来源于外部服务商的轨迹,不允许删除"); } // 删除轨迹 var @event = new TrackDeleteEvent { OrderIds = orderIds.ToArray(), Source = input.Source, TrackIdentifiers = input.TrackIdentifiers, CompareFields = input.CompareFields, CompareValue = input.CompareValue, SubSystemId = input.SubSystemId, OperationMan = userInfo.Data.UserName, }; await _capPublisher.PublishAsync(GlobalConst.TrackDeleteSubscribe, @event); return new UnifyResultDto(true, true); } /// /// 新增、更新轨迹(存在就更新) /// /// /// /// [HttpPost("Track/Upsert")] public async Task UpsertTrackAsync(UpsertTrackInput input) { var order = await _orderRepository.GetFirstAsync(x => x.SubSystemId == input.SubSystemId && x.OrderType == input.OrderType && x.OrderNo == input.OrderNo, x => new { x.Id, x.OrderNo, x.CustomId }); if (order == null) { throw new BusinessException(message: $"订单:{input.OrderNo}不存在"); } // 输入参数的静态规则检查 // 轨迹内容上的约束检查 // 更新轨迹时是否可以匹配到轨迹的检查 await TrackUpsertValidation(input.IsAdd, input.SubSystemId, order.Id, order.OrderNo, input.TrackCode, input.Source, input.TrackTime, input.NewTrackTime, input.EstimatedDeliveryDate, input.DestinationTimeZone, input.Extra, input.CompareFields); var userInfo = await _requestCurrentUserService.Service.GetCurrentUserBasicInfoAsync(); if (userInfo.Code != GlobalConst.SuccessCode || userInfo.Data is null) { //todo:保留之前的方式(FBA在用) 后续可能去除 userInfo = await _requestCurrentUserService.Service.GetCurrentUserBasicInfoAsync(input.OptionUserId); if (userInfo.Code != GlobalConst.SuccessCode || userInfo.Data is null) throw new BusinessException(message: $"调用用户中心,获取用户信息失败:{userInfo.Msg}"); } var name = userInfo.Data.UserName; // 新增/更新轨迹 var @event = ObjectMapper.Map(input); @event.SubSystemId = input.SubSystemId; @event.CustomId = order.CustomId; @event.OrderIdentifiers = new[] { new OrderIdentifier { OrderId = order.Id, OrderNo = input.OrderNo, OrderType = input.OrderType, } }; @event.OperationMan = name; await _capPublisher.PublishAsync(GlobalConst.TrackUpsertSubscribe, @event); return new UnifyResultDto(true, true); } /// /// 批量新增、更新轨迹 /// /// /// /// [HttpPost("Track/BatchUpsert")] public async Task BatchUpsertTrackAsync(BatchUpsertTrackInput input) { input.OrderNos = input.OrderNos.Where(x => !x.IsNullOrWhiteSpace()).Distinct().ToList(); // 单号去重、去空 var orders = await _orderRepository.GetListAsync(x => x.SubSystemId == input.SubSystemId && x.OrderType == input.OrderType && input.OrderNos.Contains(x.OrderNo), x => new { x.Id, x.OrderNo, x.CustomId }); if (orders.Count == 0) { throw new BusinessException(message: "订单一个都不存在"); } // 校验主体:批量新增只选取第一条订单进行校验 //【新增】存在指定轨迹的内容,进行更新覆盖(只处理一条的情况,并且决定权交给用户),不存在直接插入即可 //【更新】存在指定轨迹的内容(匹配到多条则不处理) var order = orders.First(); // 输入参数的静态规则检查 // 轨迹内容上的约束检查 // 更新轨迹时是否可以匹配到轨迹的检查 await TrackUpsertValidation(input.IsAdd, input.SubSystemId, order.Id, order.OrderNo, input.TrackCode, input.Source, input.TrackTime, input.NewTrackTime, input.EstimatedDeliveryDate, input.DestinationTimeZone, input.Extra, input.CompareFields); // 获取用户name var userInfo = await _requestCurrentUserService.Service.GetCurrentUserBasicInfoAsync(); if (userInfo.Code != GlobalConst.SuccessCode || userInfo.Data is null) { throw new BusinessException(message: $"调用用户中心,获取用户信息失败:{userInfo.Msg}"); } // 按客户新增/更新轨迹(为了推送轨迹时方便) foreach (var group in orders.GroupBy(x => x.CustomId)) { var @event = new TrackUpsertEvent { SubSystemId = input.SubSystemId, CustomId = group.Key, OrderIdentifiers = group.Select(o => new OrderIdentifier { OrderId = o.Id, OrderNo = o.OrderNo, OrderType = input.OrderType, }).ToArray(), IsAdd = input.IsAdd, TrackCode = input.TrackCode, TrackPlace = input.TrackPlace, TrackTime = input.TrackTime, NewTrackTime = input.NewTrackTime, TrackTimeZone = input.TrackTimeZone, OperationMan = userInfo.Data.UserName, OperationManId = userInfo.Data.UserId, IsVisibility = input.IsVisibility, IsExternal = input.IsExternal, Source = input.Source, Remarks = input.Remarks, Files = input.Files, Content = input.Content, EnStatus = input.EnStatus, TrackPlaceCity = input.TrackPlaceCity, TrackPlaceCityId = input.TrackPlaceCityId, TrackPlaceProvince = input.TrackPlaceProvince, TrackPlaceProvinceId = input.TrackPlaceProvinceId, TrackPlaceNation = input.TrackPlaceNation, TrackPlaceNationId = input.TrackPlaceNationId, TrackPlacePostcode = input.TrackPlacePostcode, EstimatedDeliveryDate = input.EstimatedDeliveryDate, DestinationTimeZone = input.DestinationTimeZone, CompareFields = input.CompareFields, Extra = input.Extra, ForcedPush = input.ForcedPush, IsToMq = input.IsToMq, }; await _capPublisher.PublishAsync(GlobalConst.TrackUpsertSubscribe, @event); } return new UnifyResultDto(true, true); } //在轨迹更新主流程之前的前置检查 private async Task TrackUpsertValidation( bool isAdd, int subSystemId, int orderId, string orderNo, string trackCode, int source, DateTime trackTime, DateTime? newTrackTime, DateTime? estimatedDeliveryDate, float? destinationTimeZone, string? extra, string? compareFields ) { #region 静态规则检查 // 约束轨迹来源只能选取 1-外部 2-内部 3-手动 if (!GlobalConst.SupportTrackSources.Contains(source)) { throw new BusinessException(message: "轨迹来源是必须的,1:外部服务商 2:内部系统 3:人工操作"); } //当修改时,如果为来源为外部提供商则不允许修改 if (!isAdd && source == (int) TrackSourceEnum.ExternalServiceProvide) { throw new BusinessException(message: "来源为外部服务商轨迹不允许修改"); } //要求时间在当前时间的前后100年之间内 bool ValidateTrackTime(DateTime? d) => (d != null && d < DateTime.Now.AddYears(-100) || d > DateTime.Now.AddYears(100)); if (ValidateTrackTime(trackTime) || ValidateTrackTime(newTrackTime) || ValidateTrackTime(estimatedDeliveryDate)) { throw new BusinessException(message: "轨迹时间不合法"); } if ((estimatedDeliveryDate.HasValue == false && destinationTimeZone.HasValue) || (estimatedDeliveryDate.HasValue && destinationTimeZone.HasValue == false)) { throw new BusinessException(message: "【预计签收时间】和【签收地时区】必须同时输入"); } if (!string.IsNullOrWhiteSpace(compareFields) && string.IsNullOrWhiteSpace(extra)) { throw new BusinessException(message: "当输入了【轨迹对比字段】则轨迹扩展是必须的的"); } #endregion #region 轨迹内容中的配置规则检查 var trackContent = await _trackContentRepository.GetByCodeAsync(subSystemId, trackCode); if (trackContent == null) { throw new BusinessException(message: $"轨迹代码:{trackCode}轨迹内容不存在"); } //指示是否为手动操作 var manualOperation = source == (int) TrackSourceEnum.Artificial; //不允许手动创建 if (manualOperation && isAdd && !trackContent.IsAllowManuallyCreate) { throw new BusinessException(message: $"轨迹代码:{trackCode}不允许手动创建"); } //不允许手动更新 if (manualOperation && !isAdd && !trackContent.IsAllowManuallyUpdate) { throw new BusinessException(message: $"轨迹代码:{trackCode}不允许手动更新"); } #endregion //轨迹新增,不允许重复的轨迹提前拦截 if (isAdd) { //允许重复,跳过检查 if (trackContent.IsRepeatable) return; //找到这个订单的这个轨迹code的所有轨迹 var tracks = await RetryQueryTrackAsync(orderId, trackCode, trackTime, extra, compareFields, trackContent.IsRepeatable); if (tracks.Count > 0) throw new BusinessException(message: $"订单:[{orderNo}],轨迹代码:{trackCode}不允许重复创建轨迹"); } //针对于轨迹更新检查 else { //找到这个订单的这个轨迹code的所有轨迹, var tracks = await RetryQueryTrackAsync(orderId, trackCode, trackTime, extra, compareFields, trackContent.IsRepeatable); //不存在需要更新的轨迹 if (tracks.Count == 0) throw new BusinessException(message: $"订单:[{orderNo}],轨迹代码:[{trackCode}]没有要更新的记录"); //更新轨迹,同一时间存在多条轨迹记录,理由:不清晰到底要修改哪一条或是全部修改 if (tracks.Count > 1) throw new BusinessException(message: $"订单:{orderNo},轨迹代码:[{trackCode}]存在多条记录,不允许修改"); } } // 重试查询轨迹,直到存在符合要求的轨迹,或者超过最大重试次数 private async Task> RetryQueryTrackAsync(int orderId, string trackCode, DateTime trackTime, string? extra, string? compareFields, bool isRepeatable) { var tracks = await QueryAndFilterAsync(orderId, trackCode, trackTime, extra, compareFields, isRepeatable); if (tracks.Count > 0) return tracks; //重试阶段 var curRetryCount = 0; while (curRetryCount < GlobalConst.MaxUpdateTrackRetryCount) //tips:后续将最大重试次数提出到配置中 { try { tracks = await QueryAndFilterAsync(orderId, trackCode, trackTime, extra, compareFields, isRepeatable); if (tracks.Count > 0) return tracks; //指数退避等待 var delay = TimeSpan.FromMilliseconds(Math.Pow(2, curRetryCount) * 150); // 150ms, 300ms, 600ms, ... await Task.Delay(delay); } finally { curRetryCount++; } } //超过最大重试次数还没找到 return tracks; } //查询并按比较字段过滤轨迹,当比较字段为空时则使用轨迹发生时间过滤 //tips:后续进行投影缩参优化 private async Task> QueryAndFilterAsync(int orderId, string trackCode, DateTime trackTime, string? extra, string? compareFields, bool isRepeatable) { var tracks = await _trackDetailsEsRepository.GetListAsync(new[] { orderId }, trackCode, true); // 如果不允许重复,则直接返回 if (!isRepeatable) return tracks; //无比较字段时使用轨迹时间参与过滤(兼容老版本) if (string.IsNullOrWhiteSpace(compareFields)) { return tracks.Where(x => x.TrackTime == trackTime).ToList(); } return tracks.Where(x => { if (string.IsNullOrWhiteSpace(x.Extra)) throw new BusinessException(message: "轨迹extra为空无法使用比较字段比较,请检查轨迹"); var extraJsonElement = TextJsonHelper.DeserializeToJsonElement(x.Extra); var inputExtraJsonElement = TextJsonHelper.DeserializeToJsonElement(extra); return TextJsonHelper.EqByJsonElementField(extraJsonElement, inputExtraJsonElement, compareFields); }) .ToList(); } }