diff --git a/scripts/sql/20260801_transport_warning_simplify.sql b/scripts/sql/20260801_transport_warning_simplify.sql new file mode 100644 index 00000000..3868b618 --- /dev/null +++ b/scripts/sql/20260801_transport_warning_simplify.sql @@ -0,0 +1,31 @@ +-- 运输时效:一产品一国国家 + 考核总时效;规则主体按 AppKey 方案级 +-- 执行前请备份 t_warning / t_warning_transport_scene + +-- 1) 国家列(若已存在可跳过) +ALTER TABLE t_warning_transport_scene + ADD COLUMN CountryCode VARCHAR(16) NULL COMMENT '国家二字码(单一)' AFTER ProductCode; + +-- 2) 考核总时效(小时) +ALTER TABLE t_warning_transport_scene + ADD COLUMN AssessmentTotalTimeSpan FLOAT NULL COMMENT '考核总时效(小时)' AFTER CountryCode; + +-- 3) 运输时效规则主体改为方案级:客户清空 +UPDATE t_warning +SET Customer = '' +WHERE WarningType = 1 + AND (Customer IS NULL OR Customer <> ''); + +-- 4) 同一 AppKey 下多个已启用运输时效方案,仅保留 Id 最小的一条 +UPDATE t_warning w + INNER JOIN ( + SELECT AppKey, MIN(Id) AS KeepId + FROM t_warning + WHERE WarningType = 1 + AND IsEnabled = 1 + GROUP BY AppKey + HAVING COUNT(1) > 1 + ) d ON w.AppKey = d.AppKey +SET w.IsEnabled = 0 +WHERE w.WarningType = 1 + AND w.IsEnabled = 1 + AND w.Id <> d.KeepId; diff --git a/src/TrackCenter.Application/Dtos/Warning/Transport/TransportWarningSceneDto.cs b/src/TrackCenter.Application/Dtos/Warning/Transport/TransportWarningSceneDto.cs index dcaffc58..3473b45e 100644 --- a/src/TrackCenter.Application/Dtos/Warning/Transport/TransportWarningSceneDto.cs +++ b/src/TrackCenter.Application/Dtos/Warning/Transport/TransportWarningSceneDto.cs @@ -36,22 +36,32 @@ public class TransportWarningSceneDto public string? LeavePort { get; set; } /// - /// 产品编码(多个,分割) + /// 产品编码(单一) /// public string? ProductCode { get; set; } /// - /// 月份(多个/分割) + /// 国家二字码(单一) + /// + public string? CountryCode { get; set; } + + /// + /// 考核总时效(小时) + /// + public float? AssessmentTotalTimeSpan { get; set; } + + /// + /// 月份(多个/分割,历史字段) /// public string? Months { get; set; } /// - /// 区域Id(单一) + /// 区域Id(单一,历史字段) /// public int? AreaId { get; set; } /// - /// 区域id对应的名称 + /// 区域id对应的名称(历史字段) /// public string? AreaName { get; set; } } diff --git a/src/TrackCenter.Application/Dtos/Warning/Transport/UpsertTransportWarningSceneInput.cs b/src/TrackCenter.Application/Dtos/Warning/Transport/UpsertTransportWarningSceneInput.cs index 78426db4..a117ba01 100644 --- a/src/TrackCenter.Application/Dtos/Warning/Transport/UpsertTransportWarningSceneInput.cs +++ b/src/TrackCenter.Application/Dtos/Warning/Transport/UpsertTransportWarningSceneInput.cs @@ -1,3 +1,4 @@ +using System.ComponentModel.DataAnnotations; using TrackCenter.Application.Dtos.Warning.DataAnnotations; namespace TrackCenter.Application.Dtos.Warning.Transport; @@ -15,33 +16,31 @@ public sealed class UpsertTransportWarningSceneInput [IdRequired] public int WarningId { get; set; } - /// - /// 是否中转(单一) - /// + /// 历史字段,忽略 public bool? IsTransit { get; set; } - /// - /// 货物类型(多个,分割) - /// + /// 历史字段,忽略 public string? CargoType { get; set; } - /// - /// 起运港口(多个,分割) - /// + /// 历史字段,忽略 public string? LeavePort { get; set; } - /// - /// 产品编码(多个,分割) - /// + /// 产品编码(单一销售产品) + [Required(ErrorMessage = "请配置销售产品")] public string? ProductCode { get; set; } - /// - /// 月份(多个/分割) - /// + /// 国家二字码(单一) + [Required(ErrorMessage = "请配置国家")] + public string? CountryCode { get; set; } + + /// 考核总时效(小时) + [Required(ErrorMessage = "请配置考核总时效")] + [Range(0.1, 99999, ErrorMessage = "考核总时效须大于0")] + public float? AssessmentTotalTimeSpan { get; set; } + + /// 历史字段,忽略 public string? Months { get; set; } - /// - /// 区域Id(单一) - /// + /// 历史字段,忽略 public int? AreaId { get; set; } } diff --git a/src/TrackCenter.Application/Dtos/Warning/UpsertWarningRuleInput.cs b/src/TrackCenter.Application/Dtos/Warning/UpsertWarningRuleInput.cs index 4d705397..68cf5c97 100644 --- a/src/TrackCenter.Application/Dtos/Warning/UpsertWarningRuleInput.cs +++ b/src/TrackCenter.Application/Dtos/Warning/UpsertWarningRuleInput.cs @@ -19,9 +19,9 @@ public class UpsertWarningRuleInput public string AppKey { get; set; } //应用 /// - /// 客户 + /// 客户(运输时效可空,后端会置空;回传/完整预警必填) /// - public string Customer { get; set; } //客户 + public string? Customer { get; set; } /// /// 预警规则名称 diff --git a/src/TrackCenter.Application/Services/Warning/Handlers/TransportWarningHandler.cs b/src/TrackCenter.Application/Services/Warning/Handlers/TransportWarningHandler.cs index 90bef12c..3fd374fb 100644 --- a/src/TrackCenter.Application/Services/Warning/Handlers/TransportWarningHandler.cs +++ b/src/TrackCenter.Application/Services/Warning/Handlers/TransportWarningHandler.cs @@ -223,9 +223,9 @@ public class TransportWarningHandler : ITransportWarningHandler, ITransientDepen orders = orders.Where(x => earliestTrackStatusTrackDict.ContainsKey(x.Id)).ToList(); if (orders.Count == 0) return; - // 3.查规则主体 - var conditions = orders.Select(x => (AppKey: x.AppKey, x.Customer)).ToHashSet(); - var ruleDict = await _warningRuleRepository.GetWarningRuleForEnabledAsync(conditions, WarningRuleType.运输时效预警); + // 3.查规则主体(运输时效:AppKey 方案级,不按客户) + var appKeys = orders.Select(x => x.AppKey).ToHashSet(); + var ruleDict = await _warningRuleRepository.GetWarningRuleForEnabledByAppKeysAsync(appKeys, WarningRuleType.运输时效预警); if (ruleDict.Count == 0) return; // 4.查规则场景 @@ -233,11 +233,7 @@ public class TransportWarningHandler : ITransportWarningHandler, ITransientDepen var scenes = await _warningRuleRepository.GetTransportSceneAsync(x => ruleIds.Contains(x.WarningId)); if (scenes.Count == 0) return; - // 5.查区域信息 - var areaIds = scenes.Where(x => x.AreaId > 0).Select(x => x.AreaId!.Value).ToHashSet(); - var ruleAreaInfosDict = await _kpiAreaRepository.GetAreaInfosDictByAreaId(areaIds); - - // 选出订单命中的场景 + // 选出订单命中的场景(仅产品 + 国家) var hitSceneIds = new HashSet(); var orderIdHitSceneIdsDict = new Dictionary>(); var sceneBos = TransportWarningSceneBO.From(scenes); @@ -246,7 +242,10 @@ public class TransportWarningHandler : ITransportWarningHandler, ITransientDepen var sceneSet = new HashSet(); foreach (var scene in sceneBos) { - if (!scene.CanHitScene(order, ruleAreaInfosDict.GetValueOrDefault(scene.AreaId ?? 0))) continue; + // 仅匹配同 AppKey 下方案的场景 + if (!ruleDict.TryGetValue(scene.WarningId, out var rule) || rule.AppKey != order.AppKey) + continue; + if (!scene.CanHitScene(order)) continue; sceneSet.Add(scene.Id); hitSceneIds.Add(scene.Id); } @@ -576,14 +575,11 @@ public class TransportWarningHandler : ITransportWarningHandler, ITransientDepen return; // 3.重新验证订单是否命中新场景 - var areas = postScene.AreaId.HasValue - ? _kpiAreaRepository.GetAreaInfosByAreaId(postScene.AreaId.Value) - : new List(); var noHitSceneOrderIds = new HashSet(); var reHitSceneOrderIds = new HashSet(); foreach (var order in orders) { - if (postScene.CanHitScene(order, areas)) + if (postScene.CanHitScene(order)) reHitSceneOrderIds.Add(order.Id); else noHitSceneOrderIds.Add(order.Id); @@ -599,15 +595,11 @@ public class TransportWarningHandler : ITransportWarningHandler, ITransientDepen var updateRuleItems = await _warningRuleRepository.GetTransportSceneRuleItemsAsync(x => updateRuleItemIds.Contains(x.Id)); var ruleItemDict = updateRuleItems.ToDictionary(x => x.Id); - // 场景->区域 - var ruleArea = postScene.AreaId.HasValue - ? await _kpiAreaRepository.GetFirstAsync(x => x.Id == postScene.AreaId.Value) - : null; foreach (var detail in updateDetails) { var ruleItem = ruleItemDict[detail.WarningSceneRuleItemId]; detail.WarningName = rule.Name; - detail.WarningRuleDescription = detail.GetWarningRuleDescription(postScene.ToEntity(), ruleItem, ruleArea); + detail.WarningRuleDescription = detail.GetWarningRuleDescription(postScene.ToEntity(), ruleItem, null); needUpdateDetails.Add(detail); } } @@ -679,12 +671,8 @@ public class TransportWarningHandler : ITransportWarningHandler, ITransientDepen private static bool CanSceneChange(TransportWarningSceneBO oldScene, TransportWarningSceneBO postScene) { - return oldScene.CargoType != postScene.CargoType || //多个 普货,危险品 - oldScene.Months != postScene.Months || //多个 约束提交的月份组从小到大 1/3/4,不能出现 4/1/3这种 (如果出现则SequenceEqual) - oldScene.AreaId != postScene.AreaId || // 单一,area1 - oldScene.IsTransit != postScene.IsTransit || //单一,true/false - oldScene.LeavePort != postScene.LeavePort || //多个 - oldScene.ProductCode != postScene.ProductCode; //多个 + return !string.Equals(oldScene.ProductCode, postScene.ProductCode, StringComparison.OrdinalIgnoreCase) + || !string.Equals(oldScene.CountryCode, postScene.CountryCode, StringComparison.OrdinalIgnoreCase); } #endregion todo: 规则场景变更 diff --git a/src/TrackCenter.Application/Services/Warning/Model/TransportWarningSceneBO.cs b/src/TrackCenter.Application/Services/Warning/Model/TransportWarningSceneBO.cs index 0b89666d..fb43cf61 100644 --- a/src/TrackCenter.Application/Services/Warning/Model/TransportWarningSceneBO.cs +++ b/src/TrackCenter.Application/Services/Warning/Model/TransportWarningSceneBO.cs @@ -7,122 +7,64 @@ namespace TrackCenter.Application.Services.Warning.Model; public sealed class TransportWarningSceneBO { - /// - /// 场景唯一id (传0视为新增) - /// public int Id { get; } - - /// - /// 预警规则主体id - /// public int WarningId { get; } - - /// - /// 货物类型(多个,分割) - /// public string? CargoType { get; } - - /// - /// 月份(多个/分割) - /// public string? Months { get; } - - /// - /// 区域Id(单一) - /// public int? AreaId { get; } - - /// - /// 是否中转(单一) - /// public bool? IsTransit { get; } - - /// - /// 起运港口(多个,分割) - /// public string? LeavePort { get; } - /// - /// 产品编码(多个,分割) - /// + /// 产品编码(单一) public string? ProductCode { get; } - public TransportWarningSceneBO(int id, int warningId, string? cargoType, string? months, int? areaId, bool? isTransit, string? leavePort, string? productCode) + /// 国家二字码(单一) + public string? CountryCode { get; } + + /// 考核总时效(小时) + public float? AssessmentTotalTimeSpan { get; } + + public TransportWarningSceneBO( + int id, + int warningId, + string? cargoType, + string? months, + int? areaId, + bool? isTransit, + string? leavePort, + string? productCode, + string? countryCode = null, + float? assessmentTotalTimeSpan = null) { Id = id; WarningId = warningId; CargoType = cargoType; - - // 如果有月份则排序之后在写入 - if (!string.IsNullOrWhiteSpace(months)) - { - var sorted = months.Split('/').Select(int.Parse).OrderBy(x => x).ToList(); - Months = string.Join('/', sorted); - } - + Months = months; AreaId = areaId; IsTransit = isTransit; LeavePort = leavePort; - ProductCode = productCode; + ProductCode = string.IsNullOrWhiteSpace(productCode) ? null : productCode.Trim(); + CountryCode = NormalizeSingleCountryCode(countryCode); + AssessmentTotalTimeSpan = assessmentTotalTimeSpan; } - public bool CanHitScene(OrderEntity order, List? ruleAreas) + /// + /// 命中:订单产品 = 配置产品;到达国家 = 配置国家(一对一)。 + /// + public bool CanHitScene(OrderEntity order, List? ruleAreas = null) { - // 是否中转 - if (IsTransit != null && IsTransit != order.IsTransit) + _ = ruleAreas; + + if (string.IsNullOrWhiteSpace(ProductCode) || string.IsNullOrWhiteSpace(order.ProductCode)) return false; - // 货物类型 - if (!string.IsNullOrWhiteSpace(CargoType) && order.CargoType != CargoType) + if (!string.Equals(order.ProductCode.Trim(), ProductCode, StringComparison.OrdinalIgnoreCase)) return false; - // 启运港口 - if (!string.IsNullOrWhiteSpace(LeavePort) && order.LeavePort != LeavePort) + if (string.IsNullOrWhiteSpace(CountryCode) || string.IsNullOrWhiteSpace(order.ArrivalCountry)) return false; - // 产品编码 - if (!string.IsNullOrWhiteSpace(ProductCode) && order.ProductCode != ProductCode) - return false; - - // 月份 - if (!string.IsNullOrWhiteSpace(Months)) - { - var months = string.IsNullOrEmpty(Months) - ? new HashSet() - : Months.Split('/').Select(int.Parse).ToHashSet(); - // 订单创建时间月份 - var orderCreateTimeMonth = order.CreateTime.Month; - if (!months.Contains(orderCreateTimeMonth)) - return false; - } - - // 区域 - if (ruleAreas != null && ruleAreas.Count > 0) - { - // 使用订单到达国家的二字码匹配区域 - var areas = ruleAreas.Where(area => order.ArrivalCountry == area.CountryCode).ToList(); - foreach (var area in areas) - { - // 邮编是否在关注的范围内? - if (!string.IsNullOrWhiteSpace(area.LeftPostcode) && !string.IsNullOrWhiteSpace(area.RightPostcode)) - { - // tips: 参考小包的邮编匹配,字典序 - if (string.CompareOrdinal(order.ArrivalPostCode, area.LeftPostcode) < 0 || - string.CompareOrdinal(order.ArrivalPostCode, area.RightPostcode) > 0) - return false; - } - - // 城市是否匹配? - if (!string.IsNullOrWhiteSpace(area.City) && order.ArrivalCity != area.City) - return false; - - // 省份是否匹配? - if (!string.IsNullOrWhiteSpace(area.Province) && order.ArrivalProvince != area.Province) - return false; - } - } - - return true; + return string.Equals(order.ArrivalCountry.Trim(), CountryCode, StringComparison.OrdinalIgnoreCase); } public static TransportWarningSceneEntity ToEntity(TransportWarningSceneBO bo) @@ -135,23 +77,13 @@ public sealed class TransportWarningSceneBO bo.AreaId, bo.IsTransit, bo.LeavePort, - bo.ProductCode + bo.ProductCode, + bo.CountryCode, + bo.AssessmentTotalTimeSpan ); } - public TransportWarningSceneEntity ToEntity() - { - return new TransportWarningSceneEntity( - Id, - WarningId, - CargoType, - Months, - AreaId, - IsTransit, - LeavePort, - ProductCode - ); - } + public TransportWarningSceneEntity ToEntity() => ToEntity(this); public static TransportWarningSceneBO From(TransportWarningSceneEntity entity) { @@ -163,47 +95,54 @@ public sealed class TransportWarningSceneBO entity.AreaId, entity.IsTransit, entity.LeavePort, - entity.ProductCode + entity.ProductCode, + entity.CountryCode, + entity.AssessmentTotalTimeSpan ); } public static List From(List entities) - { - return entities.Select(From).ToList(); - } + => entities.Select(From).ToList(); public static TransportWarningSceneBO From(UpsertTransportWarningSceneInput input) { return new TransportWarningSceneBO( input.Id, input.WarningId, - input.CargoType, - input.Months, - input.AreaId, - input.IsTransit, - input.LeavePort, - input.ProductCode + null, + null, + null, + null, + null, + input.ProductCode, + input.CountryCode, + input.AssessmentTotalTimeSpan ); } private bool Equals(TransportWarningSceneBO other) { return WarningId == other.WarningId - && IsTransit == other.IsTransit - && CargoType == other.CargoType - && LeavePort == other.LeavePort - && ProductCode == other.ProductCode - && Months == other.Months - && AreaId == other.AreaId; + && string.Equals(ProductCode, other.ProductCode, StringComparison.OrdinalIgnoreCase) + && string.Equals(CountryCode, other.CountryCode, StringComparison.OrdinalIgnoreCase); } public override bool Equals(object? obj) - { - return ReferenceEquals(this, obj) || obj is TransportWarningSceneBO other && Equals(other); - } + => ReferenceEquals(this, obj) || obj is TransportWarningSceneBO other && Equals(other); public override int GetHashCode() + => HashCode.Combine(WarningId, ProductCode?.ToUpperInvariant(), CountryCode?.ToUpperInvariant()); + + private static string? NormalizeSingleCountryCode(string? countryCode) { - return HashCode.Combine(WarningId, IsTransit, CargoType, LeavePort, ProductCode, Months, AreaId); + if (string.IsNullOrWhiteSpace(countryCode)) + return null; + + var code = countryCode + .Split([',', '/', ';', '|', ' '], StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries) + .Select(x => x.ToUpperInvariant()) + .FirstOrDefault(x => x.Length > 0); + + return string.IsNullOrWhiteSpace(code) ? null : code; } } diff --git a/src/TrackCenter.Application/Services/Warning/TransportWarningService.cs b/src/TrackCenter.Application/Services/Warning/TransportWarningService.cs index 9e50875f..b24cb8d7 100644 --- a/src/TrackCenter.Application/Services/Warning/TransportWarningService.cs +++ b/src/TrackCenter.Application/Services/Warning/TransportWarningService.cs @@ -89,6 +89,8 @@ public class TransportWarningService : ApplicationServiceBase CargoType = x.CargoType, LeavePort = x.LeavePort, ProductCode = x.ProductCode, + CountryCode = x.CountryCode, + AssessmentTotalTimeSpan = x.AssessmentTotalTimeSpan, Months = x.Months, AreaId = x.AreaId, AreaName = x.AreaId.HasValue ? areaNameDict?.GetValueOrDefault(x.AreaId.Value) : null @@ -96,37 +98,17 @@ public class TransportWarningService : ApplicationServiceBase return map.ToList(); - // 构建场景摘要 + // 构建场景摘要:产品 / 国家 / 总时效Xh string BuildSummary(TransportWarningSceneEntity entity) { - const string separator = "/"; - // var sb = new StringBuilder(); - var list = new List(6); - // 货物类型 - if (!string.IsNullOrWhiteSpace(entity.CargoType)) - list.Add($"{entity.CargoType}"); - // 月份 - if (!string.IsNullOrWhiteSpace(entity.Months)) - list.Add($"{entity.Months.Replace("/", ",")}月份"); - // 区域名称 - if (entity.AreaId != null && areaNameDict != null) - { - var areaName = areaNameDict.GetValueOrDefault(entity.AreaId.Value); - if (!string.IsNullOrWhiteSpace(areaName)) - list.Add($"{areaName}"); - } - - // 中转 - if (entity.IsTransit != null) - list.Add($"中转为{(entity.IsTransit.Value ? "是" : "否")}"); - // 起运港口 - if (!string.IsNullOrWhiteSpace(entity.LeavePort)) - list.Add($"{entity.LeavePort}"); - // 产品编码 + var list = new List(3); if (!string.IsNullOrWhiteSpace(entity.ProductCode)) - list.Add($"{entity.ProductCode}"); - - return string.Join(separator, list); + list.Add(entity.ProductCode); + if (!string.IsNullOrWhiteSpace(entity.CountryCode)) + list.Add(entity.CountryCode); + if (entity.AssessmentTotalTimeSpan is > 0) + list.Add($"{entity.AssessmentTotalTimeSpan.Value / 24f:0.##}天"); + return list.Count == 0 ? "-" : string.Join(" / ", list); } } @@ -282,14 +264,31 @@ public class TransportWarningService : ApplicationServiceBase var postBo = TransportWarningSceneBO.From(input); + if (string.IsNullOrWhiteSpace(postBo.ProductCode)) + throw new BusinessException(message: "请配置销售产品"); + if (postBo.ProductCode.Contains(',') || postBo.ProductCode.Contains('/')) + throw new BusinessException(message: "运输时效仅支持单一销售产品"); + if (string.IsNullOrWhiteSpace(postBo.CountryCode)) + throw new BusinessException(message: "请配置国家"); + if (input.CountryCode != null && + input.CountryCode.IndexOfAny([',', '/', ';', '|', ' ']) >= 0) + throw new BusinessException(message: "运输时效仅支持单一国家"); + if (postBo.AssessmentTotalTimeSpan is null or <= 0) + throw new BusinessException(message: "请配置有效的考核总时效"); + // 前端按整数天录入后转小时(天×24) + var totalHours = postBo.AssessmentTotalTimeSpan.Value; + if (Math.Abs(totalHours - MathF.Round(totalHours)) > 0.001f || + Math.Abs(totalHours % 24f) > 0.001f) + throw new BusinessException(message: "考核总时效须为整天对应的小时数(天×24)"); + // 查出这个规则主体下的所有场景 var oldScenes = await _warningRuleRepository.GetTransportSceneAsync(input.WarningId); var oldBos = oldScenes.Count > 0 ? TransportWarningSceneBO.From(oldScenes) : Empty.List; - // 验证规则场景是否重复 + // 验证规则场景是否重复(同一方案下:产品+国家唯一) if (oldBos.Any(old => old.Equals(postBo) && old.Id != postBo.Id)) { - throw new BusinessException(message: "同一规则主体下,不允许出现因素完全相同的场景!"); + throw new BusinessException(message: "同一方案下,不允许出现产品与国家完全相同的配置!"); } // 更新或者新增 diff --git a/src/TrackCenter.Application/Services/Warning/WarningService.cs b/src/TrackCenter.Application/Services/Warning/WarningService.cs index 6eceb864..bfac5b12 100644 --- a/src/TrackCenter.Application/Services/Warning/WarningService.cs +++ b/src/TrackCenter.Application/Services/Warning/WarningService.cs @@ -84,10 +84,13 @@ public class WarningService : ApplicationServiceBase throw new BusinessException(message: $"预警规则主体启用状态未发生变化,不允许重复操作"); } - // 如果设置为启用则检查同一个应用同一客户下有多个启用的规则 + // 启用查重:运输时效按 AppKey+类型;其它类型按 AppKey+客户+类型 if (input.IsEnabled && await _warningRuleRepository.CheckEnableDuplicateAsync(rule.Id, rule.AppKey, rule.Customer, (WarningRuleType) rule.WarningType)) { - throw new BusinessException(message: $"同一【应用+客户+类型】下不能有多个启用的预警规则主体,AppKey=[{rule.AppKey},Customer=[{rule.Customer}]]"); + var scope = (WarningRuleType) rule.WarningType == WarningRuleType.运输时效预警 + ? $"同一【应用+类型】下不能有多个启用的预警规则主体,AppKey=[{rule.AppKey}]" + : $"同一【应用+客户+类型】下不能有多个启用的预警规则主体,AppKey=[{rule.AppKey},Customer=[{rule.Customer}]]"; + throw new BusinessException(message: scope); } // 更新规则主体状态 @@ -120,17 +123,28 @@ public class WarningService : ApplicationServiceBase throw new BusinessException(message: $"WarningId=[{input.Id}],WarningType=[{input.WarningType}]预警规则主体不存在,不允许修改"); } - // 要求 AppKey+Customer+Name+WarningType 联合不能重复 - if (await _warningRuleRepository.CheckNameDuplicateAsync(input.Id, input.AppKey, input.Customer, input.Name, (WarningRuleType) input.WarningType)) + // 运输时效方案级:客户固定为空,不参与匹配 + var customer = (WarningRuleType) input.WarningType == WarningRuleType.运输时效预警 + ? string.Empty + : (input.Customer ?? string.Empty); + + if ((WarningRuleType) input.WarningType != WarningRuleType.运输时效预警 && string.IsNullOrWhiteSpace(customer)) + throw new BusinessException(message: "客户不能为空"); + + // 名称查重:运输时效按 AppKey+Name+Type;其它按 AppKey+Customer+Name+Type + if (await _warningRuleRepository.CheckNameDuplicateAsync(input.Id, input.AppKey, customer, input.Name, (WarningRuleType) input.WarningType)) { - throw new BusinessException(message: $"AppKey:[{input.AppKey}],Customer:[{input.Customer}]," + + throw new BusinessException(message: $"AppKey:[{input.AppKey}],Customer:[{customer}]," + $"WarningType:[{input.WarningType.ToString()}]已存在名称为[{input.Name}]规则,不允许重复创建/修改"); } // 如果创建/修改的主体为启用状态,则要求通过检查 - if (input.IsEnabled && await _warningRuleRepository.CheckEnableDuplicateAsync(input.Id, input.AppKey, input.Customer, (WarningRuleType) input.WarningType)) + if (input.IsEnabled && await _warningRuleRepository.CheckEnableDuplicateAsync(input.Id, input.AppKey, customer, (WarningRuleType) input.WarningType)) { - throw new BusinessException(message: $"同一【应用+客户+预警类型】下不能有多个启用的预警规则主体,不允许创建/修改"); + var scope = (WarningRuleType) input.WarningType == WarningRuleType.运输时效预警 + ? "同一【应用+预警类型】下不能有多个启用的预警规则主体,不允许创建/修改" + : "同一【应用+客户+预警类型】下不能有多个启用的预警规则主体,不允许创建/修改"; + throw new BusinessException(message: scope); } var enabledChanged = false; @@ -140,7 +154,7 @@ public class WarningService : ApplicationServiceBase input.Id, input.Name, input.AppKey, - input.Customer, + customer, input.IsEnabled, input.WarningType ); @@ -150,7 +164,7 @@ public class WarningService : ApplicationServiceBase enabledChanged = ruleEntity.IsEnabled != input.IsEnabled; ruleEntity.Name = input.Name; ruleEntity.AppKey = input.AppKey; - ruleEntity.Customer = input.Customer; + ruleEntity.Customer = customer; ruleEntity.IsEnabled = input.IsEnabled; ruleEntity.WarningType = input.WarningType; } diff --git a/src/TrackCenter.Domain/Aggregates/WarningAggregate/IWarningRuleRepository.cs b/src/TrackCenter.Domain/Aggregates/WarningAggregate/IWarningRuleRepository.cs index 75c63a7e..7c040109 100644 --- a/src/TrackCenter.Domain/Aggregates/WarningAggregate/IWarningRuleRepository.cs +++ b/src/TrackCenter.Domain/Aggregates/WarningAggregate/IWarningRuleRepository.cs @@ -32,6 +32,11 @@ public interface IWarningRuleRepository : IBasicRepository // 获取启用的预警规则主体 Task> GetWarningRuleForEnabledAsync(HashSet<(string AppKey, string Customer)> conditions, WarningRuleType warningType); + /// + /// 按 AppKey 获取启用的预警规则主体(运输时效:方案级,不按客户) + /// + Task> GetWarningRuleForEnabledByAppKeysAsync(HashSet appKeys, WarningRuleType warningType); + // 获取预警规则主体根据id Task GetWarningRuleByIdAsync(int id, WarningRuleType? warningType = null); diff --git a/src/TrackCenter.Domain/Aggregates/WarningAggregate/Transport/TransportWarningDetailsIndex.cs b/src/TrackCenter.Domain/Aggregates/WarningAggregate/Transport/TransportWarningDetailsIndex.cs index 550378e1..bdcb1f3c 100644 --- a/src/TrackCenter.Domain/Aggregates/WarningAggregate/Transport/TransportWarningDetailsIndex.cs +++ b/src/TrackCenter.Domain/Aggregates/WarningAggregate/Transport/TransportWarningDetailsIndex.cs @@ -155,6 +155,12 @@ public sealed class TransportWarningDetailsIndex : IdBaseIndex // 产品代码(xxx) if (!string.IsNullOrWhiteSpace(scene.ProductCode)) res.Append($",{scene.ProductCode}"); + // 国家(US) + if (!string.IsNullOrWhiteSpace(scene.CountryCode)) + res.Append($",{scene.CountryCode}"); + // 考核总时效 + if (scene.AssessmentTotalTimeSpan is > 0) + res.Append($",考核总时效{scene.AssessmentTotalTimeSpan.Value / 24f:0.##}天"); // 规则配置信息 res.Append($",生成预警时间{ruleItem.StartTrackWarningTimeSpan}小时" + diff --git a/src/TrackCenter.Domain/Aggregates/WarningAggregate/Transport/TransportWarningSceneEntity.cs b/src/TrackCenter.Domain/Aggregates/WarningAggregate/Transport/TransportWarningSceneEntity.cs index a8c25568..c4330c38 100644 --- a/src/TrackCenter.Domain/Aggregates/WarningAggregate/Transport/TransportWarningSceneEntity.cs +++ b/src/TrackCenter.Domain/Aggregates/WarningAggregate/Transport/TransportWarningSceneEntity.cs @@ -13,29 +13,45 @@ public sealed class TransportWarningSceneEntity : FullAuditedEntity [SugarColumn(ColumnName = "WarningId", ColumnDescription = "预警规则id")] public int WarningId { get; set; } - [SugarColumn(ColumnName = "IsTransit", ColumnDescription = "是否中转")] - public bool? IsTransit { get; set; } //是否中转 + [SugarColumn(ColumnName = "IsTransit", ColumnDescription = "是否中转", IsNullable = true)] + public bool? IsTransit { get; set; } //历史字段 - [SugarColumn(ColumnName = "CargoType", ColumnDescription = "货物类型")] - public string? CargoType { get; set; } //货物类型, + [SugarColumn(ColumnName = "CargoType", ColumnDescription = "货物类型", IsNullable = true)] + public string? CargoType { get; set; } //历史字段 - [SugarColumn(ColumnName = "LeavePort", ColumnDescription = "起运港口")] - public string? LeavePort { get; set; } //起运港口 + [SugarColumn(ColumnName = "LeavePort", ColumnDescription = "起运港口", IsNullable = true)] + public string? LeavePort { get; set; } //历史字段 - [SugarColumn(ColumnName = "ProductCode", ColumnDescription = "产品编码")] - public string? ProductCode { get; set; } //产品编码 + [SugarColumn(ColumnName = "ProductCode", ColumnDescription = "产品编码", IsNullable = true)] + public string? ProductCode { get; set; } //单一销售产品 - [SugarColumn(ColumnName = "Months", ColumnDescription = "月份")] - public string? Months { get; set; } //月份, 1/2/3 + [SugarColumn(ColumnName = "CountryCode", ColumnDescription = "国家二字码(单一)", IsNullable = true, Length = 16)] + public string? CountryCode { get; set; } //单一国家二字码 - [SugarColumn(ColumnName = "AreaId", ColumnDescription = "区域id")] - public int? AreaId { get; set; } //区域Id,单分区 + [SugarColumn(ColumnName = "AssessmentTotalTimeSpan", ColumnDescription = "考核总时效(小时)", IsNullable = true)] + public float? AssessmentTotalTimeSpan { get; set; } //考核总时效 + + [SugarColumn(ColumnName = "Months", ColumnDescription = "月份", IsNullable = true)] + public string? Months { get; set; } //历史字段 + + [SugarColumn(ColumnName = "AreaId", ColumnDescription = "区域id", IsNullable = true)] + public int? AreaId { get; set; } //历史字段 public TransportWarningSceneEntity() { } - public TransportWarningSceneEntity(int id, int warningId, string? cargoType, string? months, int? areaId, bool? isTransit, string? leavePort, string? productCode) + public TransportWarningSceneEntity( + int id, + int warningId, + string? cargoType, + string? months, + int? areaId, + bool? isTransit, + string? leavePort, + string? productCode, + string? countryCode = null, + float? assessmentTotalTimeSpan = null) { Id = id; WarningId = warningId; @@ -45,5 +61,7 @@ public sealed class TransportWarningSceneEntity : FullAuditedEntity IsTransit = isTransit; LeavePort = leavePort; ProductCode = productCode; + CountryCode = countryCode; + AssessmentTotalTimeSpan = assessmentTotalTimeSpan; } } diff --git a/src/TrackCenter.Repository/Repositories/WarningRuleRepository.cs b/src/TrackCenter.Repository/Repositories/WarningRuleRepository.cs index a8e5a24e..ee2540a7 100644 --- a/src/TrackCenter.Repository/Repositories/WarningRuleRepository.cs +++ b/src/TrackCenter.Repository/Repositories/WarningRuleRepository.cs @@ -15,12 +15,16 @@ public class WarningRuleRepository : BasicRepository, IWarnin public async Task CheckNameDuplicateAsync(int id, string appKey, string customer, string name, WarningRuleType warningType) { - var entity = await Context.Queryable() + // 运输时效:AppKey + Name + Type(方案级,不按客户) + var query = Context.Queryable() .Where(x => x.AppKey == appKey - && x.Customer == customer && x.WarningType == (int) warningType - && string.Equals(x.Name, name)) - .FirstAsync(); + && string.Equals(x.Name, name)); + + if (warningType != WarningRuleType.运输时效预警) + query = query.Where(x => x.Customer == customer); + + var entity = await query.FirstAsync(); // 不重复 if (entity == null) @@ -32,14 +36,18 @@ public class WarningRuleRepository : BasicRepository, IWarnin public async Task CheckEnableDuplicateAsync(int warningId, string appKey, string customer, WarningRuleType warningType) { - var entity = await Context.Queryable() + // 运输时效:同一 AppKey + 类型仅允许一个启用方案 + var query = Context.Queryable() .Where(x => x.AppKey == appKey - && x.Customer == customer && x.WarningType == (int) warningType - && x.IsEnabled) - .FirstAsync(); + && x.IsEnabled); - // 没有【应用+客户+类型】启用的规则 + if (warningType != WarningRuleType.运输时效预警) + query = query.Where(x => x.Customer == customer); + + var entity = await query.FirstAsync(); + + // 没有启用的规则 if (entity == null) return false; // 已经启用和当前规则不同,视为启用重复,不允许操作 @@ -117,6 +125,22 @@ public class WarningRuleRepository : BasicRepository, IWarnin : list.ToDictionary(x => x.Id); } + public async Task> GetWarningRuleForEnabledByAppKeysAsync(HashSet appKeys, WarningRuleType warningType) + { + if (appKeys.Count == 0) + return Empty.Dict; + + var list = await Context.Queryable() + .Where(x => x.WarningType == (int) warningType + && x.IsEnabled + && appKeys.Contains(x.AppKey)) + .ToListAsync(); + + return list.Count == 0 + ? Empty.Dict + : list.ToDictionary(x => x.Id); + } + public async Task GetWarningRuleByIdAsync(int id, WarningRuleType? warningType = null) { var result = await Context.Queryable()