feat: simplify transport warning to product+country with assessment SLA

AppKey-level scheme; scene matches one product and one country; stores assessment total hours (days×24) with migration SQL.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
zzh 2026-08-01 11:13:38 +08:00
parent dabb9a4edb
commit aeae900b5e
12 changed files with 266 additions and 233 deletions

View File

@ -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;

View File

@ -36,22 +36,32 @@ public class TransportWarningSceneDto
public string? LeavePort { get; set; }
/// <summary>
/// 产品编码(多个,分割)
/// 产品编码(单一)
/// </summary>
public string? ProductCode { get; set; }
/// <summary>
/// 月份(多个/分割)
/// 国家二字码(单一)
/// </summary>
public string? CountryCode { get; set; }
/// <summary>
/// 考核总时效(小时)
/// </summary>
public float? AssessmentTotalTimeSpan { get; set; }
/// <summary>
/// 月份(多个/分割,历史字段)
/// </summary>
public string? Months { get; set; }
/// <summary>
/// 区域Id(单一)
/// 区域Id(单一,历史字段)
/// </summary>
public int? AreaId { get; set; }
/// <summary>
/// 区域id对应的名称
/// 区域id对应的名称(历史字段)
/// </summary>
public string? AreaName { get; set; }
}

View File

@ -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; }
/// <summary>
/// 是否中转(单一)
/// </summary>
/// <summary>历史字段,忽略</summary>
public bool? IsTransit { get; set; }
/// <summary>
/// 货物类型(多个,分割)
/// </summary>
/// <summary>历史字段,忽略</summary>
public string? CargoType { get; set; }
/// <summary>
/// 起运港口(多个,分割)
/// </summary>
/// <summary>历史字段,忽略</summary>
public string? LeavePort { get; set; }
/// <summary>
/// 产品编码(多个,分割)
/// </summary>
/// <summary>产品编码(单一销售产品)</summary>
[Required(ErrorMessage = "请配置销售产品")]
public string? ProductCode { get; set; }
/// <summary>
/// 月份(多个/分割)
/// </summary>
/// <summary>国家二字码(单一)</summary>
[Required(ErrorMessage = "请配置国家")]
public string? CountryCode { get; set; }
/// <summary>考核总时效(小时)</summary>
[Required(ErrorMessage = "请配置考核总时效")]
[Range(0.1, 99999, ErrorMessage = "考核总时效须大于0")]
public float? AssessmentTotalTimeSpan { get; set; }
/// <summary>历史字段,忽略</summary>
public string? Months { get; set; }
/// <summary>
/// 区域Id(单一)
/// </summary>
/// <summary>历史字段,忽略</summary>
public int? AreaId { get; set; }
}

View File

@ -19,9 +19,9 @@ public class UpsertWarningRuleInput
public string AppKey { get; set; } //应用
/// <summary>
/// 客户
/// 客户(运输时效可空,后端会置空;回传/完整预警必填)
/// </summary>
public string Customer { get; set; } //客户
public string? Customer { get; set; }
/// <summary>
/// 预警规则名称

View File

@ -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<int>();
var orderIdHitSceneIdsDict = new Dictionary<int, HashSet<int>>();
var sceneBos = TransportWarningSceneBO.From(scenes);
@ -246,7 +242,10 @@ public class TransportWarningHandler : ITransportWarningHandler, ITransientDepen
var sceneSet = new HashSet<int>();
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<KpiAreaInfo>();
var noHitSceneOrderIds = new HashSet<int>();
var reHitSceneOrderIds = new HashSet<int>();
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: 规则场景变更

View File

@ -7,122 +7,64 @@ namespace TrackCenter.Application.Services.Warning.Model;
public sealed class TransportWarningSceneBO
{
/// <summary>
/// 场景唯一id (传0视为新增)
/// </summary>
public int Id { get; }
/// <summary>
/// 预警规则主体id
/// </summary>
public int WarningId { get; }
/// <summary>
/// 货物类型(多个,分割)
/// </summary>
public string? CargoType { get; }
/// <summary>
/// 月份(多个/分割)
/// </summary>
public string? Months { get; }
/// <summary>
/// 区域Id(单一)
/// </summary>
public int? AreaId { get; }
/// <summary>
/// 是否中转(单一)
/// </summary>
public bool? IsTransit { get; }
/// <summary>
/// 起运港口(多个,分割)
/// </summary>
public string? LeavePort { get; }
/// <summary>
/// 产品编码(多个,分割)
/// </summary>
/// <summary>产品编码(单一)</summary>
public string? ProductCode { get; }
public TransportWarningSceneBO(int id, int warningId, string? cargoType, string? months, int? areaId, bool? isTransit, string? leavePort, string? productCode)
/// <summary>国家二字码(单一)</summary>
public string? CountryCode { get; }
/// <summary>考核总时效(小时)</summary>
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<KpiAreaInfo>? ruleAreas)
/// <summary>
/// 命中:订单产品 = 配置产品;到达国家 = 配置国家(一对一)。
/// </summary>
public bool CanHitScene(OrderEntity order, List<KpiAreaInfo>? 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<int>()
: 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<TransportWarningSceneBO> From(List<TransportWarningSceneEntity> 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;
}
}

View File

@ -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<string>(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<string>(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<TransportWarningSceneBO>.List;
// 验证规则场景是否重复
// 验证规则场景是否重复(同一方案下:产品+国家唯一)
if (oldBos.Any(old => old.Equals(postBo) && old.Id != postBo.Id))
{
throw new BusinessException(message: "同一规则主体下,不允许出现因素完全相同的场景!");
throw new BusinessException(message: "同一方案下,不允许出现产品与国家完全相同的配置!");
}
// 更新或者新增

View File

@ -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;
}

View File

@ -32,6 +32,11 @@ public interface IWarningRuleRepository : IBasicRepository<WarningRuleEntity>
// 获取启用的预警规则主体
Task<IReadOnlyDictionary<int, WarningRuleEntity>> GetWarningRuleForEnabledAsync(HashSet<(string AppKey, string Customer)> conditions, WarningRuleType warningType);
/// <summary>
/// 按 AppKey 获取启用的预警规则主体(运输时效:方案级,不按客户)
/// </summary>
Task<IReadOnlyDictionary<int, WarningRuleEntity>> GetWarningRuleForEnabledByAppKeysAsync(HashSet<string> appKeys, WarningRuleType warningType);
// 获取预警规则主体根据id
Task<WarningRuleEntity?> GetWarningRuleByIdAsync(int id, WarningRuleType? warningType = null);

View File

@ -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}小时" +

View File

@ -13,29 +13,45 @@ public sealed class TransportWarningSceneEntity : FullAuditedEntity<int, int>
[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<int, int>
IsTransit = isTransit;
LeavePort = leavePort;
ProductCode = productCode;
CountryCode = countryCode;
AssessmentTotalTimeSpan = assessmentTotalTimeSpan;
}
}

View File

@ -15,12 +15,16 @@ public class WarningRuleRepository : BasicRepository<WarningRuleEntity>, IWarnin
public async Task<bool> CheckNameDuplicateAsync(int id, string appKey, string customer, string name, WarningRuleType warningType)
{
var entity = await Context.Queryable<WarningRuleEntity>()
// 运输时效:AppKey + Name + Type(方案级,不按客户)
var query = Context.Queryable<WarningRuleEntity>()
.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<WarningRuleEntity>, IWarnin
public async Task<bool> CheckEnableDuplicateAsync(int warningId, string appKey, string customer, WarningRuleType warningType)
{
var entity = await Context.Queryable<WarningRuleEntity>()
// 运输时效:同一 AppKey + 类型仅允许一个启用方案
var query = Context.Queryable<WarningRuleEntity>()
.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<WarningRuleEntity>, IWarnin
: list.ToDictionary(x => x.Id);
}
public async Task<IReadOnlyDictionary<int, WarningRuleEntity>> GetWarningRuleForEnabledByAppKeysAsync(HashSet<string> appKeys, WarningRuleType warningType)
{
if (appKeys.Count == 0)
return Empty<int, WarningRuleEntity>.Dict;
var list = await Context.Queryable<WarningRuleEntity>()
.Where(x => x.WarningType == (int) warningType
&& x.IsEnabled
&& appKeys.Contains(x.AppKey))
.ToListAsync();
return list.Count == 0
? Empty<int, WarningRuleEntity>.Dict
: list.ToDictionary(x => x.Id);
}
public async Task<WarningRuleEntity?> GetWarningRuleByIdAsync(int id, WarningRuleType? warningType = null)
{
var result = await Context.Queryable<WarningRuleEntity>()