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()